@gakim-digital/dexter-bridge 0.11.6 → 0.11.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.11.6",
3
+ "version": "0.11.8",
4
4
  "description": "Local bridge for InstaWebAI and Dexter — runs Codex, Claude Code, or OpenCode on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import crossSpawn from 'cross-spawn';
7
7
  import {
8
8
  getFramerProjectAuthorization,
9
+ isCompanionPairingInvalidError,
9
10
  postRunEvent,
10
11
  reportFramerProjectAuthorizationVerification,
11
12
  } from './api.js';
@@ -1523,6 +1524,9 @@ async function callProviderAdapter(
1523
1524
  if (controlRequestsCancellation(response)) await requestCancel();
1524
1525
  })
1525
1526
  .catch((error) => {
1527
+ if (isCompanionPairingInvalidError(error)) {
1528
+ void requestCancel();
1529
+ }
1526
1530
  if (!finished) {
1527
1531
  trace?.warn('adapter_activity_post_failed', {
1528
1532
  error: errorMeta(error),
@@ -1562,6 +1566,10 @@ async function callProviderAdapter(
1562
1566
  if (controlRequestsCancellation(response)) await requestCancel();
1563
1567
  } catch (error) {
1564
1568
  if (monitorAbort.signal.aborted) return;
1569
+ if (isCompanionPairingInvalidError(error)) {
1570
+ await requestCancel();
1571
+ return;
1572
+ }
1565
1573
  trace?.warn('adapter_control_poll_failed', { error: errorMeta(error) });
1566
1574
  }
1567
1575
  }
@@ -2006,6 +2014,9 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
2006
2014
  if (controlRequestsCancellation(response)) requestCliCancel();
2007
2015
  })
2008
2016
  .catch((error) => {
2017
+ if (isCompanionPairingInvalidError(error)) {
2018
+ requestCliCancel();
2019
+ }
2009
2020
  if (!cliFinished) {
2010
2021
  options.trace?.warn('cli_activity_post_failed', {
2011
2022
  error: errorMeta(error),
@@ -2044,6 +2055,10 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
2044
2055
  if (cliCancelled) return;
2045
2056
  } catch (error) {
2046
2057
  if (monitorAbort.signal.aborted) return;
2058
+ if (isCompanionPairingInvalidError(error)) {
2059
+ requestCliCancel();
2060
+ return;
2061
+ }
2047
2062
  options.trace?.warn('cli_control_poll_failed', {
2048
2063
  error: errorMeta(error),
2049
2064
  });
@@ -2255,6 +2270,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2255
2270
  }
2256
2271
  })
2257
2272
  .catch((error) => {
2273
+ if (isCompanionPairingInvalidError(error)) {
2274
+ void requestCancel('device_disconnect');
2275
+ }
2258
2276
  if (!finished) {
2259
2277
  options.trace?.warn?.('outcome_activity_post_failed', {
2260
2278
  error: errorMeta(error),
@@ -2331,6 +2349,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2331
2349
  await requestCancel(controlCancellationSource(response));
2332
2350
  }
2333
2351
  } catch (error) {
2352
+ if (isCompanionPairingInvalidError(error)) {
2353
+ await requestCancel('device_disconnect');
2354
+ return;
2355
+ }
2334
2356
  if (!controlMonitorAbort.signal.aborted && !cancelRequested) {
2335
2357
  options.trace?.warn?.('outcome_control_poll_failed', {
2336
2358
  error: errorMeta(error),
@@ -2991,6 +3013,7 @@ export function checkCommand(command, args = ['--version'], timeoutMs = 10000) {
2991
3013
 
2992
3014
  export async function checkAgentAvailability(agent, model, options = {}) {
2993
3015
  const normalizedAgent = normalizeAgentName(agent);
3016
+ const modelCatalogRefreshedAt = nowIso();
2994
3017
  if (normalizedAgent === 'dry-run') {
2995
3018
  return {
2996
3019
  ok: true,
@@ -3002,6 +3025,8 @@ export async function checkAgentAvailability(agent, model, options = {}) {
3002
3025
  installed: true,
3003
3026
  signedIn: true,
3004
3027
  status: 'ready',
3028
+ modelCatalogSource: 'runtime',
3029
+ modelCatalogRefreshedAt,
3005
3030
  };
3006
3031
  }
3007
3032
  const definition = definitionForAgent(normalizedAgent);
@@ -3025,6 +3050,8 @@ export async function checkAgentAvailability(agent, model, options = {}) {
3025
3050
  installed: false,
3026
3051
  signedIn: false,
3027
3052
  status: 'unavailable',
3053
+ modelCatalogSource: 'fallback',
3054
+ modelCatalogRefreshedAt,
3028
3055
  };
3029
3056
  }
3030
3057
  if (normalizedAgent === 'codex') {
@@ -3051,9 +3078,19 @@ export async function checkAgentAvailability(agent, model, options = {}) {
3051
3078
  error:
3052
3079
  detection.error
3053
3080
  || 'Codex is not signed in. Run `codex login` on this computer, then run the bridge command again.',
3081
+ modelCatalogSource: 'fallback',
3082
+ modelCatalogRefreshedAt,
3054
3083
  };
3055
3084
  }
3056
- const reportedModels = await adapter.models();
3085
+ let reportedModels = supportedModels;
3086
+ let modelCatalogSource = 'fallback';
3087
+ let modelCatalogError = null;
3088
+ try {
3089
+ reportedModels = await adapter.models();
3090
+ modelCatalogSource = 'runtime';
3091
+ } catch (error) {
3092
+ modelCatalogError = error?.message || String(error);
3093
+ }
3057
3094
  return {
3058
3095
  ...runtime,
3059
3096
  ...detection,
@@ -3062,7 +3099,9 @@ export async function checkAgentAvailability(agent, model, options = {}) {
3062
3099
  label: definition.label,
3063
3100
  models: reportedModels.map((candidate) => candidate.id),
3064
3101
  modelDetails: reportedModels,
3065
- modelCatalogSource: 'runtime',
3102
+ modelCatalogSource,
3103
+ modelCatalogRefreshedAt,
3104
+ ...(modelCatalogError ? { modelCatalogError } : {}),
3066
3105
  installed: true,
3067
3106
  signedIn: true,
3068
3107
  status: 'ready',
@@ -3108,6 +3147,7 @@ export async function checkAgentAvailability(agent, model, options = {}) {
3108
3147
  models: reportedModels.map((candidate) => candidate.id),
3109
3148
  modelDetails: reportedModels,
3110
3149
  modelCatalogSource,
3150
+ modelCatalogRefreshedAt,
3111
3151
  ...(modelCatalogError ? { modelCatalogError } : {}),
3112
3152
  installed: true,
3113
3153
  };
package/src/api.js CHANGED
@@ -16,6 +16,15 @@ export class DexterBridgeApiError extends Error {
16
16
  }
17
17
  }
18
18
 
19
+ export function isCompanionPairingInvalidError(error) {
20
+ return error instanceof DexterBridgeApiError
21
+ && error.status === 401
22
+ && (
23
+ error.body?.code === 'FRAMER_COMPANION_NOT_PAIRED'
24
+ || /not paired|pair again/i.test(error.message)
25
+ );
26
+ }
27
+
19
28
  function bearerHeaders(deviceToken) {
20
29
  return deviceToken ? { Authorization: `Bearer ${deviceToken}` } : {};
21
30
  }
package/src/cli.js CHANGED
@@ -19,8 +19,8 @@ import {
19
19
  import { checkAgentAvailability, checkAllAgents, executeRun } from './agent.js';
20
20
  import {
21
21
  claimPairing,
22
- DexterBridgeApiError,
23
22
  heartbeat,
23
+ isCompanionPairingInvalidError,
24
24
  pollRun,
25
25
  } from './api.js';
26
26
  import {
@@ -228,12 +228,7 @@ function wait(delayMs) {
228
228
  }
229
229
 
230
230
  function isInvalidPairingError(error) {
231
- return error instanceof DexterBridgeApiError
232
- && error.status === 401
233
- && (
234
- error.body?.code === 'FRAMER_COMPANION_NOT_PAIRED'
235
- || /not paired|pair again/i.test(error.message)
236
- );
231
+ return isCompanionPairingInvalidError(error);
237
232
  }
238
233
 
239
234
  function clearInvalidPairing(configDir, cause) {
@@ -266,7 +261,48 @@ function agentEnvironment(config = {}, baseEnv = process.env) {
266
261
  return env;
267
262
  }
268
263
 
269
- async function inspectAvailability(config = {}, runtimeSelection = null) {
264
+ function modelCatalogStatus(runtimeProfiles = []) {
265
+ return Object.fromEntries(
266
+ runtimeProfiles.flatMap((profile) => {
267
+ const agent =
268
+ profile.driver === 'codex-app-server'
269
+ ? 'codex'
270
+ : profile.driver === 'claude-code-cli'
271
+ ? 'claude-code'
272
+ : null;
273
+ return agent ? [[agent, profile.modelCatalog]] : [];
274
+ }),
275
+ );
276
+ }
277
+
278
+ function preserveLastKnownModelCatalog(previousProfiles = [], nextProfiles = []) {
279
+ const previousById = new Map(previousProfiles.map((profile) => [profile.id, profile]));
280
+ return nextProfiles.map((profile) => {
281
+ const previous = previousById.get(profile.id);
282
+ if (
283
+ !previous?.models?.length
284
+ || profile.modelCatalog?.source === 'runtime'
285
+ || !profile.modelCatalog?.error
286
+ ) {
287
+ return profile;
288
+ }
289
+ return {
290
+ ...profile,
291
+ models: previous.models,
292
+ modelCatalog: {
293
+ ...profile.modelCatalog,
294
+ source: 'stale',
295
+ modelCount: previous.models.length,
296
+ },
297
+ };
298
+ });
299
+ }
300
+
301
+ async function inspectAvailability(
302
+ config = {},
303
+ runtimeSelection = null,
304
+ { forceModelRefresh = false, previousRuntimeProfiles = [] } = {},
305
+ ) {
270
306
  try {
271
307
  const bridgeEnv = agentEnvironment(config);
272
308
  const checks =
@@ -275,22 +311,33 @@ async function inspectAvailability(config = {}, runtimeSelection = null) {
275
311
  agents: [
276
312
  await checkAgentAvailability(runtimeSelection, undefined, {
277
313
  env: bridgeEnv,
314
+ forceModelRefresh,
278
315
  }),
279
316
  ],
280
317
  dryRun: null,
281
318
  }
282
319
  : runtimeSelection === 'opencode'
283
320
  ? { agents: [], dryRun: null }
284
- : await checkAllAgents({ env: bridgeEnv });
321
+ : await checkAllAgents({ env: bridgeEnv, forceModelRefresh });
285
322
  const allChecks = [...checks.agents, ...(checks.dryRun ? [checks.dryRun] : [])];
286
323
  const available = allChecks.filter((check) => check.ok);
287
- const runtimeProfiles = await discoverRuntimeProfiles({
324
+ const discoveredRuntimeProfiles = await discoverRuntimeProfiles({
288
325
  agentChecks: checks.agents,
289
326
  config,
290
327
  env: bridgeEnv,
291
328
  runtimeSelection,
292
329
  });
330
+ const runtimeProfiles = preserveLastKnownModelCatalog(
331
+ previousRuntimeProfiles,
332
+ discoveredRuntimeProfiles,
333
+ );
293
334
  const profileModels = runtimeProfiles.flatMap((profile) => profile.models || []);
335
+ const catalogStatus = modelCatalogStatus(runtimeProfiles);
336
+ const refreshedAt = Object.values(catalogStatus)
337
+ .map((status) => status?.refreshedAt)
338
+ .filter(Boolean)
339
+ .sort()
340
+ .at(-1);
294
341
  return {
295
342
  metadata: {
296
343
  availableAgents: available.map((check) => check.agent).join(','),
@@ -298,6 +345,8 @@ async function inspectAvailability(config = {}, runtimeSelection = null) {
298
345
  availableModelDetails: JSON.stringify(
299
346
  profileModels,
300
347
  ),
348
+ modelCatalogStatus: JSON.stringify(catalogStatus),
349
+ ...(refreshedAt ? { modelCatalogRefreshedAt: refreshedAt } : {}),
301
350
  agentVersions: available.map((check) => `${check.agent}=${check.version || 'unknown'}`).join(','),
302
351
  bridgeCapabilities: BRIDGE_CAPABILITIES.join(','),
303
352
  bridgeVersion: BRIDGE_VERSION,
@@ -555,7 +604,10 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
555
604
  while (true) {
556
605
  if (Date.now() >= nextCatalogRefreshAt) {
557
606
  try {
558
- const refreshedAvailability = await inspectAvailability(activeConfig, runtimeSelection);
607
+ const refreshedAvailability = await inspectAvailability(activeConfig, runtimeSelection, {
608
+ forceModelRefresh: true,
609
+ previousRuntimeProfiles: availability.runtimeProfiles,
610
+ });
559
611
  const refreshedRuntimeProfile = runtimeSelection
560
612
  ? requireAvailableRuntime(refreshedAvailability, runtimeSelection)
561
613
  : null;
@@ -598,7 +650,9 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
598
650
  status: error.status,
599
651
  code: error.body?.code || null,
600
652
  });
601
- throw clearInvalidPairing(configDir, error);
653
+ const disconnected = clearInvalidPairing(configDir, error);
654
+ console.log(disconnected.message);
655
+ return;
602
656
  }
603
657
  if (once) throw error;
604
658
  pollFailureCount += 1;
@@ -610,6 +664,45 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
610
664
  await wait(retryInMs);
611
665
  continue;
612
666
  }
667
+ if (poll?.control?.refreshModelCatalog) {
668
+ try {
669
+ const refreshedAvailability = await inspectAvailability(activeConfig, runtimeSelection, {
670
+ forceModelRefresh: true,
671
+ previousRuntimeProfiles: availability.runtimeProfiles,
672
+ });
673
+ availability = refreshedAvailability;
674
+ runtimeProfile = runtimeSelection
675
+ ? requireAvailableRuntime(refreshedAvailability, runtimeSelection)
676
+ : null;
677
+ check = runtimeProfile
678
+ ? null
679
+ : requireAvailableAgent(refreshedAvailability, agent);
680
+ model = runtimeProfile
681
+ ? runtimeProfile.models.find((candidate) => candidate.id === requestedModel)?.id
682
+ || runtimeProfile.models[0].id
683
+ : availableModel(check, requestedModel);
684
+ metadata = availability.metadata;
685
+ profiles = runtimeProfileMap(availability.runtimeProfiles);
686
+ await heartbeat(apiBaseUrl, {
687
+ deviceToken,
688
+ status: 'ready',
689
+ agent,
690
+ model,
691
+ metadata,
692
+ runtimeProfiles: availability.runtimeProfiles.map(publicRuntimeProfile),
693
+ allowInsecureHttp: flags['allow-insecure-http'],
694
+ });
695
+ pollLogger.info('model_catalog_refresh_completed', {
696
+ requestedAt: poll.control.refreshModelCatalog.requestedAt,
697
+ models: availability.runtimeProfiles.flatMap((profile) =>
698
+ profile.models.map((candidate) => candidate.id)),
699
+ });
700
+ } catch (error) {
701
+ pollLogger.warn('model_catalog_refresh_failed', { error: errorMeta(error) });
702
+ } finally {
703
+ nextCatalogRefreshAt = Date.now() + MODEL_CATALOG_REFRESH_MS;
704
+ }
705
+ }
613
706
  if (poll.run) {
614
707
  console.log(`Claimed run ${poll.run.runId}.`);
615
708
  const runtimeProfile = poll.run?.runtimeProfile || null;
package/src/config.js CHANGED
@@ -52,6 +52,7 @@ export const BRIDGE_CAPABILITIES = [
52
52
  'runtime-profiles-v1',
53
53
  'native-skills-v1',
54
54
  'model-progress-v1',
55
+ 'model-catalog-refresh-v1',
55
56
  ];
56
57
  export const BRIDGE_PRODUCTS = {
57
58
  dexter: {
@@ -503,7 +503,7 @@ const TOOL_BY_NAME = new Map(
503
503
  ]),
504
504
  );
505
505
  const sessionsByProject = new Map();
506
- let hostedSetupPromise = null;
506
+ let framerSetupPromise = null;
507
507
 
508
508
  function isRecord(value) {
509
509
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
@@ -537,6 +537,12 @@ function framerSkillRoot(env) {
537
537
  ].find((root) => fs.existsSync(path.join(root, 'SKILL.md'))) || null;
538
538
  }
539
539
 
540
+ function isMissingFramerSkillError(error) {
541
+ return /could not find an installed framer skill|run [`'"]?@framer\/agent setup/i.test(
542
+ String(error?.message || ''),
543
+ );
544
+ }
545
+
540
546
  function installedFramerAgentVersion() {
541
547
  try {
542
548
  let current = path.dirname(resolveFramerAgentCli());
@@ -1184,6 +1190,9 @@ export function createFramerAgentToolRuntime({
1184
1190
  doctrineLoaded: false,
1185
1191
  doctrineVersion: null,
1186
1192
  };
1193
+ let framerSetupAttempts = 0;
1194
+ let framerSetupRecoveredSession = false;
1195
+ let framerSetupCompleted = hasInstalledFramerSkill(env);
1187
1196
  const framerAgentVersion = installedFramerAgentVersion();
1188
1197
  let operationSequence = 0;
1189
1198
  let lastMutationSequence = 0;
@@ -1210,23 +1219,49 @@ export function createFramerAgentToolRuntime({
1210
1219
  return result;
1211
1220
  };
1212
1221
 
1213
- const ensureHostedSetup = async () => {
1214
- if (env.DEXTER_HOSTED_FRAMER_AUTO_SETUP !== 'true') return;
1215
- if (hasInstalledFramerSkill(env)) return;
1216
- if (!hostedSetupPromise) {
1222
+ const ensureFramerSetup = async ({ force = false } = {}) => {
1223
+ const automaticSetupEnabled =
1224
+ runCli === runFramerAgentCli
1225
+ || env.DEXTER_FRAMER_AUTO_SETUP === 'true';
1226
+ if (!force && !automaticSetupEnabled) return;
1227
+ if (!force && hasInstalledFramerSkill(env)) {
1228
+ framerSetupCompleted = true;
1229
+ return;
1230
+ }
1231
+ if (!framerSetupPromise) {
1232
+ framerSetupAttempts += 1;
1217
1233
  reportActivity({
1218
1234
  kind: 'status_update',
1219
1235
  source: 'framer-agent',
1220
- message: 'Preparing the Framer harness.',
1236
+ message: 'Getting Framer ready.',
1221
1237
  });
1222
- hostedSetupPromise = executeCli(['setup'], {
1238
+ framerSetupPromise = executeCli(['setup'], {
1223
1239
  timeoutMs: 30_000,
1224
- }).catch((error) => {
1225
- hostedSetupPromise = null;
1226
- throw error;
1227
- });
1240
+ })
1241
+ .then(() => {
1242
+ if (!hasInstalledFramerSkill(env)) {
1243
+ throw toolError(
1244
+ 'FRAMER_AGENT_SETUP_FAILED',
1245
+ 'Framer setup completed without installing the required Framer skill.',
1246
+ 503,
1247
+ );
1248
+ }
1249
+ framerSetupCompleted = true;
1250
+ })
1251
+ .catch((error) => {
1252
+ if (error?.code === 'FRAMER_AGENT_SETUP_FAILED') throw error;
1253
+ throw toolError(
1254
+ 'FRAMER_AGENT_SETUP_FAILED',
1255
+ `Dexter could not prepare the Framer editing tools: ${String(error?.message || 'Framer setup failed.')}`,
1256
+ 503,
1257
+ );
1258
+ })
1259
+ .finally(() => {
1260
+ framerSetupPromise = null;
1261
+ });
1228
1262
  }
1229
- await hostedSetupPromise;
1263
+ await framerSetupPromise;
1264
+ framerSetupCompleted = hasInstalledFramerSkill(env);
1230
1265
  };
1231
1266
 
1232
1267
  const projectIsAuthorizedLocally = async () => {
@@ -1351,7 +1386,7 @@ export function createFramerAgentToolRuntime({
1351
1386
  };
1352
1387
 
1353
1388
  const ensureSession = async () => {
1354
- await ensureHostedSetup();
1389
+ await ensureFramerSetup();
1355
1390
  if (sessionId) return sessionId;
1356
1391
  if (sessionPromise) return sessionPromise;
1357
1392
  sessionPromise = (async () => {
@@ -1397,7 +1432,14 @@ export function createFramerAgentToolRuntime({
1397
1432
  if (authorization) {
1398
1433
  await installProjectAuthorization(authorization);
1399
1434
  }
1400
- created = await createSession();
1435
+ try {
1436
+ created = await createSession();
1437
+ } catch (error) {
1438
+ if (!isMissingFramerSkillError(error)) throw error;
1439
+ await ensureFramerSetup({ force: true });
1440
+ created = await createSession();
1441
+ framerSetupRecoveredSession = true;
1442
+ }
1401
1443
  } catch (error) {
1402
1444
  if (isAuthorizationFailure(error)) {
1403
1445
  await reportRejectedAuthorization(authorization, error);
@@ -2829,6 +2871,11 @@ export function createFramerAgentToolRuntime({
2829
2871
  taskPlan: taskPlanSnapshot(),
2830
2872
  verification: verification(),
2831
2873
  framerAgentVersion,
2874
+ framerSetup: {
2875
+ completed: framerSetupCompleted,
2876
+ attempts: framerSetupAttempts,
2877
+ recoveredSession: framerSetupRecoveredSession,
2878
+ },
2832
2879
  framerGuidanceLoaded: guidanceMetadata.loaded,
2833
2880
  doctrineLoaded: promptMetadata.doctrineLoaded,
2834
2881
  guidanceHash: guidanceMetadata.contentHash,
@@ -2863,5 +2910,5 @@ export function createFramerAgentToolRuntime({
2863
2910
 
2864
2911
  export function __resetFramerAgentToolRuntimeForTests() {
2865
2912
  sessionsByProject.clear();
2866
- hostedSetupPromise = null;
2913
+ framerSetupPromise = null;
2867
2914
  }
@@ -71,6 +71,20 @@ function normalizeCapabilities(value = {}) {
71
71
  };
72
72
  }
73
73
 
74
+ function normalizeModelCatalog(value = {}) {
75
+ const source = ['runtime', 'fallback', 'stale', 'error'].includes(value.source)
76
+ ? value.source
77
+ : 'fallback';
78
+ return {
79
+ source,
80
+ refreshedAt: text(value.refreshedAt, 80) || new Date().toISOString(),
81
+ modelCount: Number.isFinite(Number(value.modelCount))
82
+ ? Math.max(0, Math.floor(Number(value.modelCount)))
83
+ : 0,
84
+ ...(text(value.error, 500) ? { error: text(value.error, 500) } : {}),
85
+ };
86
+ }
87
+
74
88
  export function normalizeRuntimeProfile(value, { includeSecret = false } = {}) {
75
89
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
76
90
  const driver = text(value.driver, 80);
@@ -110,6 +124,10 @@ export function normalizeRuntimeProfile(value, { includeSecret = false } = {}) {
110
124
  continuationIdentity: text(value.continuationIdentity, 180) || stableIdentity(driver, instanceId),
111
125
  capabilities: normalizeCapabilities(value.capabilities),
112
126
  models,
127
+ modelCatalog: normalizeModelCatalog({
128
+ ...(value.modelCatalog || {}),
129
+ modelCount: models.length,
130
+ }),
113
131
  ...(text(value.keyLast4, 16) ? { keyLast4: text(value.keyLast4, 16) } : {}),
114
132
  ...(text(value.command, 2048) ? { command: text(value.command, 2048) } : {}),
115
133
  ...(Array.isArray(value.args)
@@ -169,6 +187,12 @@ function profileFromAgentCheck(check) {
169
187
  cancellation: true,
170
188
  },
171
189
  models: check.modelDetails || [],
190
+ modelCatalog: {
191
+ source: check.modelCatalogSource || 'fallback',
192
+ refreshedAt: check.modelCatalogRefreshedAt,
193
+ modelCount: check.modelDetails?.length || 0,
194
+ ...(check.modelCatalogError ? { error: check.modelCatalogError } : {}),
195
+ },
172
196
  });
173
197
  }
174
198