@ai-matrx/agents 0.59.256 → 0.59.258

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.
@@ -398,6 +398,14 @@ interface ModelCatalog {
398
398
  load(variant: ModelCatalogVariant): Promise<void>;
399
399
  /** Re-read the variant now (rows stay visible while it runs). Never throws. */
400
400
  reload(variant: ModelCatalogVariant): Promise<void>;
401
+ /**
402
+ * The catalog changed upstream (`catalog_changed`): re-read every picker variant already read and every
403
+ * record already read (`records.refresh()`). Called by itself on every `announceModelCatalogChanged`
404
+ * (catalog/changes.ts) unless `followChanges: false`. Never throws.
405
+ */
406
+ refresh(): Promise<void>;
407
+ /** Stop following `catalog_changed` (a test, or a catalog a host discards). */
408
+ dispose(): void;
401
409
  /**
402
410
  * The RECORDS part (agent core B3): options list, full records with resolved controls, class
403
411
  * configs, the class index and identities — what chat's retired `modelRegistry` slice held.
@@ -650,8 +658,30 @@ interface ModelListDropdownBaseProps {
650
658
  }
651
659
  declare function ModelListDropdown({ value: savedValue, localModels: localModelsProp, access: accessProp, onValueChange, inputModalities, outputModalities, selectionPurpose, allowedModelIds, catalogVariant, emptyOptionLabel, onClear, placeholder, priorityModelIds, pinnedOfferingId, onOfferingPinChange, modelOnly, className, triggerVariant, presentation, id, disabled, disabledTitle, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, }: ModelListDropdownProps): react.JSX.Element;
652
660
 
661
+ /**
662
+ * `@ai-matrx/agents/models` — the ONE subscription to `catalog_changed`.
663
+ *
664
+ * The server publishes `catalog_changed` on the platform bus for every admin edit and direct SQL write to
665
+ * the model catalog. A host binds its bus client ONCE with `followModelCatalogChanges(subscribe)` (the web's
666
+ * directive registry, Matrx 2's IPC forward of the main-process bus); every model catalog
667
+ * (`createModelCatalog`: picker rows + records) re-reads itself on the notice, and any other reader
668
+ * subscribes here (`onModelCatalogChanged`, or `useModelCatalogRevision` / `useOnModelCatalogChanged` in
669
+ * `@ai-matrx/agents/models/react`). No surface keeps a model cache that misses the event.
670
+ *
671
+ * The hub lives on `globalThis` so two bundled copies of this module share one set of listeners.
672
+ */
673
+ /** A `catalog_changed` payload: `{ catalog?: "models" | ..., change?, model_id? }`. */
674
+ type ModelCatalogChangePayload = Record<string, unknown>;
675
+
653
676
  /** Subscribe to a slice of a model catalog's records. `select` must return a stable value for unchanged state. */
654
677
  declare function useModelRecordsSelector<T>(records: ModelRecords, select: (state: ModelRecordsState) => T): T;
678
+ /**
679
+ * How many `catalog_changed` notices this page has seen. A reader that holds its OWN rows (the admin model
680
+ * editor) puts it in its effect dependencies / query key, so an edit made anywhere re-reads it.
681
+ */
682
+ declare function useModelCatalogRevision(): number;
683
+ /** Run `onChange` on every model catalog change notice (the latest callback; never stale). */
684
+ declare function useOnModelCatalogChanged(onChange: (payload: ModelCatalogChangePayload) => void): void;
655
685
 
656
686
  /**
657
687
  * `@ai-matrx/agents/models` — downloadable (local) models: the catalog shape every client reads, the
@@ -1010,4 +1040,4 @@ interface DiscoveryResultViewProps {
1010
1040
  /** A resolved model: its header, the source's reason when the originals will not run, and every version judged. */
1011
1041
  declare function DiscoveryResultView({ result, device, runtimes, want, chooseLabel, mode, onChoose, renderVariantActions, renderActions }: DiscoveryResultViewProps): react.JSX.Element;
1012
1042
 
1013
- export { type DiscoveryKind, type DiscoveryMode, type DiscoveryPort, DiscoveryResultView, type DiscoveryResultViewProps, type DiscoverySearchHit, type DiscoverySearchPage, type DiscoverySearchQuery, type DiscoverySort, type LocalModelEntry, type LocalModelsPort, type ModelAccessPort, type ModelCatalogLinkComponent, ModelCatalogProvider, type ModelCatalogProviderProps, type ModelCatalogUiPorts, ModelDiscovery, type ModelDiscoveryProps, ModelListDropdown, type ModelListDropdownProps, YOUR_KEY_LABEL, useLocalModels, useModelAccess, useModelCatalog, useModelCatalogStore, useModelCatalogUiPorts, useModelFavorites, useModelRecordsSelector };
1043
+ export { type DiscoveryKind, type DiscoveryMode, type DiscoveryPort, DiscoveryResultView, type DiscoveryResultViewProps, type DiscoverySearchHit, type DiscoverySearchPage, type DiscoverySearchQuery, type DiscoverySort, type LocalModelEntry, type LocalModelsPort, type ModelAccessPort, type ModelCatalogLinkComponent, ModelCatalogProvider, type ModelCatalogProviderProps, type ModelCatalogUiPorts, ModelDiscovery, type ModelDiscoveryProps, ModelListDropdown, type ModelListDropdownProps, YOUR_KEY_LABEL, useLocalModels, useModelAccess, useModelCatalog, useModelCatalogRevision, useModelCatalogStore, useModelCatalogUiPorts, useModelFavorites, useModelRecordsSelector, useOnModelCatalogChanged };
@@ -398,6 +398,14 @@ interface ModelCatalog {
398
398
  load(variant: ModelCatalogVariant): Promise<void>;
399
399
  /** Re-read the variant now (rows stay visible while it runs). Never throws. */
400
400
  reload(variant: ModelCatalogVariant): Promise<void>;
401
+ /**
402
+ * The catalog changed upstream (`catalog_changed`): re-read every picker variant already read and every
403
+ * record already read (`records.refresh()`). Called by itself on every `announceModelCatalogChanged`
404
+ * (catalog/changes.ts) unless `followChanges: false`. Never throws.
405
+ */
406
+ refresh(): Promise<void>;
407
+ /** Stop following `catalog_changed` (a test, or a catalog a host discards). */
408
+ dispose(): void;
401
409
  /**
402
410
  * The RECORDS part (agent core B3): options list, full records with resolved controls, class
403
411
  * configs, the class index and identities — what chat's retired `modelRegistry` slice held.
@@ -650,8 +658,30 @@ interface ModelListDropdownBaseProps {
650
658
  }
651
659
  declare function ModelListDropdown({ value: savedValue, localModels: localModelsProp, access: accessProp, onValueChange, inputModalities, outputModalities, selectionPurpose, allowedModelIds, catalogVariant, emptyOptionLabel, onClear, placeholder, priorityModelIds, pinnedOfferingId, onOfferingPinChange, modelOnly, className, triggerVariant, presentation, id, disabled, disabledTitle, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, }: ModelListDropdownProps): react.JSX.Element;
652
660
 
661
+ /**
662
+ * `@ai-matrx/agents/models` — the ONE subscription to `catalog_changed`.
663
+ *
664
+ * The server publishes `catalog_changed` on the platform bus for every admin edit and direct SQL write to
665
+ * the model catalog. A host binds its bus client ONCE with `followModelCatalogChanges(subscribe)` (the web's
666
+ * directive registry, Matrx 2's IPC forward of the main-process bus); every model catalog
667
+ * (`createModelCatalog`: picker rows + records) re-reads itself on the notice, and any other reader
668
+ * subscribes here (`onModelCatalogChanged`, or `useModelCatalogRevision` / `useOnModelCatalogChanged` in
669
+ * `@ai-matrx/agents/models/react`). No surface keeps a model cache that misses the event.
670
+ *
671
+ * The hub lives on `globalThis` so two bundled copies of this module share one set of listeners.
672
+ */
673
+ /** A `catalog_changed` payload: `{ catalog?: "models" | ..., change?, model_id? }`. */
674
+ type ModelCatalogChangePayload = Record<string, unknown>;
675
+
653
676
  /** Subscribe to a slice of a model catalog's records. `select` must return a stable value for unchanged state. */
654
677
  declare function useModelRecordsSelector<T>(records: ModelRecords, select: (state: ModelRecordsState) => T): T;
678
+ /**
679
+ * How many `catalog_changed` notices this page has seen. A reader that holds its OWN rows (the admin model
680
+ * editor) puts it in its effect dependencies / query key, so an edit made anywhere re-reads it.
681
+ */
682
+ declare function useModelCatalogRevision(): number;
683
+ /** Run `onChange` on every model catalog change notice (the latest callback; never stale). */
684
+ declare function useOnModelCatalogChanged(onChange: (payload: ModelCatalogChangePayload) => void): void;
655
685
 
656
686
  /**
657
687
  * `@ai-matrx/agents/models` — downloadable (local) models: the catalog shape every client reads, the
@@ -1010,4 +1040,4 @@ interface DiscoveryResultViewProps {
1010
1040
  /** A resolved model: its header, the source's reason when the originals will not run, and every version judged. */
1011
1041
  declare function DiscoveryResultView({ result, device, runtimes, want, chooseLabel, mode, onChoose, renderVariantActions, renderActions }: DiscoveryResultViewProps): react.JSX.Element;
1012
1042
 
1013
- export { type DiscoveryKind, type DiscoveryMode, type DiscoveryPort, DiscoveryResultView, type DiscoveryResultViewProps, type DiscoverySearchHit, type DiscoverySearchPage, type DiscoverySearchQuery, type DiscoverySort, type LocalModelEntry, type LocalModelsPort, type ModelAccessPort, type ModelCatalogLinkComponent, ModelCatalogProvider, type ModelCatalogProviderProps, type ModelCatalogUiPorts, ModelDiscovery, type ModelDiscoveryProps, ModelListDropdown, type ModelListDropdownProps, YOUR_KEY_LABEL, useLocalModels, useModelAccess, useModelCatalog, useModelCatalogStore, useModelCatalogUiPorts, useModelFavorites, useModelRecordsSelector };
1043
+ export { type DiscoveryKind, type DiscoveryMode, type DiscoveryPort, DiscoveryResultView, type DiscoveryResultViewProps, type DiscoverySearchHit, type DiscoverySearchPage, type DiscoverySearchQuery, type DiscoverySort, type LocalModelEntry, type LocalModelsPort, type ModelAccessPort, type ModelCatalogLinkComponent, ModelCatalogProvider, type ModelCatalogProviderProps, type ModelCatalogUiPorts, ModelDiscovery, type ModelDiscoveryProps, ModelListDropdown, type ModelListDropdownProps, YOUR_KEY_LABEL, useLocalModels, useModelAccess, useModelCatalog, useModelCatalogRevision, useModelCatalogStore, useModelCatalogUiPorts, useModelFavorites, useModelRecordsSelector, useOnModelCatalogChanged };
@@ -2280,7 +2280,26 @@ function ModelListDropdown({
2280
2280
  }
2281
2281
 
2282
2282
  // models/react/records.ts
2283
- import { useSyncExternalStore as useSyncExternalStore2 } from "react";
2283
+ import { useEffect as useEffect3, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2 } from "react";
2284
+
2285
+ // models/catalog/changes.ts
2286
+ var KEY = /* @__PURE__ */ Symbol.for("ai-matrx.agents.model-catalog-changes");
2287
+ function hub() {
2288
+ const g = globalThis;
2289
+ return g[KEY] ??= { listeners: /* @__PURE__ */ new Set(), revision: 0 };
2290
+ }
2291
+ function onModelCatalogChanged(listener) {
2292
+ const h = hub();
2293
+ h.listeners.add(listener);
2294
+ return () => {
2295
+ h.listeners.delete(listener);
2296
+ };
2297
+ }
2298
+ function modelCatalogRevision() {
2299
+ return hub().revision;
2300
+ }
2301
+
2302
+ // models/react/records.ts
2284
2303
  function useModelRecordsSelector(records, select) {
2285
2304
  return useSyncExternalStore2(
2286
2305
  records.subscribe,
@@ -2288,10 +2307,19 @@ function useModelRecordsSelector(records, select) {
2288
2307
  () => select(records.getState())
2289
2308
  );
2290
2309
  }
2310
+ var subscribeRevision = (listener) => onModelCatalogChanged(() => listener());
2311
+ function useModelCatalogRevision() {
2312
+ return useSyncExternalStore2(subscribeRevision, modelCatalogRevision, modelCatalogRevision);
2313
+ }
2314
+ function useOnModelCatalogChanged(onChange) {
2315
+ const latest = useRef2(onChange);
2316
+ latest.current = onChange;
2317
+ useEffect3(() => onModelCatalogChanged((payload) => latest.current(payload)), []);
2318
+ }
2291
2319
 
2292
2320
  // models/react/ModelDiscovery.tsx
2293
2321
  import { formatCount as formatCount2, formatFileSize } from "@ai-matrx/kit/format";
2294
- import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
2322
+ import { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
2295
2323
  import { Avatar, AvatarFallback, AvatarImage, Collapsible, CollapsibleContent, CollapsibleTrigger, ErrorNotice } from "@ai-matrx/design-system";
2296
2324
  import { Badge, Button, Field, SearchField, Select, Tabs } from "@ai-matrx/design-system/controls";
2297
2325
 
@@ -2570,8 +2598,8 @@ function ModelDiscovery({
2570
2598
  const result = controlled !== void 0 ? controlled : own;
2571
2599
  const [resolving, setResolving] = useState2(null);
2572
2600
  const [resolveError, setResolveError] = useState2(null);
2573
- const abortRef = useRef2(null);
2574
- useEffect3(() => () => abortRef.current?.abort(), []);
2601
+ const abortRef = useRef3(null);
2602
+ useEffect4(() => () => abortRef.current?.abort(), []);
2575
2603
  const resolve = async (link) => {
2576
2604
  const trimmed = link.trim();
2577
2605
  if (!trimmed) return;
@@ -2661,7 +2689,7 @@ function SearchTab({ port, onPick }) {
2661
2689
  const [page, setPage] = useState2(null);
2662
2690
  const [busy, setBusy] = useState2(false);
2663
2691
  const [error, setError] = useState2(null);
2664
- const seq = useRef2(0);
2692
+ const seq = useRef3(0);
2665
2693
  const run = async (cursor) => {
2666
2694
  if (!port.search) return;
2667
2695
  const mine = ++seq.current;
@@ -2677,7 +2705,7 @@ function SearchTab({ port, onPick }) {
2677
2705
  if (mine === seq.current) setBusy(false);
2678
2706
  }
2679
2707
  };
2680
- useEffect3(() => {
2708
+ useEffect4(() => {
2681
2709
  if (port.search) void run(null);
2682
2710
  }, [kind, sort]);
2683
2711
  if (!port.search) return /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: "Search is not available here yet. Paste a link instead." });
@@ -2814,8 +2842,10 @@ export {
2814
2842
  useLocalModels,
2815
2843
  useModelAccess,
2816
2844
  useModelCatalog,
2845
+ useModelCatalogRevision,
2817
2846
  useModelCatalogStore,
2818
2847
  useModelCatalogUiPorts,
2819
2848
  useModelFavorites,
2820
- useModelRecordsSelector
2849
+ useModelRecordsSelector,
2850
+ useOnModelCatalogChanged
2821
2851
  };
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  "use strict";
3
- "use client";
4
3
  var __defProp = Object.defineProperty;
5
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1,5 +1,4 @@
1
1
  "use client";
2
- "use client";
3
2
 
4
3
  // runtime/react/index.ts
5
4
  import { useEffect, useSyncExternalStore } from "react";
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  "use strict";
3
- "use client";
4
3
  var __defProp = Object.defineProperty;
5
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1,5 +1,4 @@
1
1
  "use client";
2
- "use client";
3
2
 
4
3
  // settings/react/index.ts
5
4
  import { useSyncExternalStore } from "react";
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  "use strict";
3
- "use client";
4
3
  var __defProp = Object.defineProperty;
5
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1,5 +1,4 @@
1
1
  "use client";
2
- "use client";
3
2
 
4
3
  // skills/react/useSkillCatalog.ts
5
4
  import { useEffect, useMemo, useSyncExternalStore } from "react";
@@ -47,7 +47,13 @@ interface SourceRef {
47
47
  promote?: ResourcePromotion | ResourcePromotion[];
48
48
  exclude?: string[];
49
49
  }
50
- interface SourceSet {
50
+ /**
51
+ * A TYPE, not an interface (2026-10-23): it IS a JSON object, so it is assignable wherever the server
52
+ * contract types the field as a JSON object (@ai-matrx/api's request bodies carry `source_set` as
53
+ * `{ [key: string]: JsonValue }` — the podcast generate request). An interface has no implicit index
54
+ * signature and would need a cast at every send.
55
+ */
56
+ type SourceSet = {
51
57
  __kind: typeof SOURCE_SET_KIND;
52
58
  version: typeof SOURCE_SET_VERSION;
53
59
  sources: SourceRef[];
@@ -59,7 +65,7 @@ interface SourceSet {
59
65
  retrieve_query?: string;
60
66
  /** The model whose context window sets the budget. */
61
67
  target_model_id?: string;
62
- }
68
+ };
63
69
  type SourceState = "ready" | "processing" | "failed" | "unavailable";
64
70
  interface SourceManifestForm {
65
71
  form: string;
@@ -47,7 +47,13 @@ interface SourceRef {
47
47
  promote?: ResourcePromotion | ResourcePromotion[];
48
48
  exclude?: string[];
49
49
  }
50
- interface SourceSet {
50
+ /**
51
+ * A TYPE, not an interface (2026-10-23): it IS a JSON object, so it is assignable wherever the server
52
+ * contract types the field as a JSON object (@ai-matrx/api's request bodies carry `source_set` as
53
+ * `{ [key: string]: JsonValue }` — the podcast generate request). An interface has no implicit index
54
+ * signature and would need a cast at every send.
55
+ */
56
+ type SourceSet = {
51
57
  __kind: typeof SOURCE_SET_KIND;
52
58
  version: typeof SOURCE_SET_VERSION;
53
59
  sources: SourceRef[];
@@ -59,7 +65,7 @@ interface SourceSet {
59
65
  retrieve_query?: string;
60
66
  /** The model whose context window sets the budget. */
61
67
  target_model_id?: string;
62
- }
68
+ };
63
69
  type SourceState = "ready" | "processing" | "failed" | "unavailable";
64
70
  interface SourceManifestForm {
65
71
  form: string;
@@ -47,7 +47,13 @@ interface SourceRef {
47
47
  promote?: ResourcePromotion | ResourcePromotion[];
48
48
  exclude?: string[];
49
49
  }
50
- interface SourceSet {
50
+ /**
51
+ * A TYPE, not an interface (2026-10-23): it IS a JSON object, so it is assignable wherever the server
52
+ * contract types the field as a JSON object (@ai-matrx/api's request bodies carry `source_set` as
53
+ * `{ [key: string]: JsonValue }` — the podcast generate request). An interface has no implicit index
54
+ * signature and would need a cast at every send.
55
+ */
56
+ type SourceSet = {
51
57
  __kind: typeof SOURCE_SET_KIND;
52
58
  version: typeof SOURCE_SET_VERSION;
53
59
  sources: SourceRef[];
@@ -59,7 +65,7 @@ interface SourceSet {
59
65
  retrieve_query?: string;
60
66
  /** The model whose context window sets the budget. */
61
67
  target_model_id?: string;
62
- }
68
+ };
63
69
  type SourceState = "ready" | "processing" | "failed" | "unavailable";
64
70
  interface SourceManifestForm {
65
71
  form: string;
@@ -705,21 +711,6 @@ interface FileRecoveryDeps {
705
711
  fileOrganizationId?: ((fileId: string) => string | null | undefined) | undefined;
706
712
  }
707
713
 
708
- /**
709
- * `@ai-matrx/agents/sources/react` — thin React hooks over the Source runtime
710
- * (`@ai-matrx/agents/sources/runtime`). No logic lives here that a non-React
711
- * client could not reach through the runtime itself: the hooks subscribe,
712
- * schedule, and hand back the same controller.
713
- *
714
- * const set = useSourceSet(adapter, { persistenceReady });
715
- * const intake = useSourceIntake(set, deps);
716
- * useSourceRecovery(set, intake, runner, { organizationId, readFileState });
717
- * const search = useSourcePartsSearch(client, ref, query);
718
- *
719
- * A host binds the adapter to its own store once (memoized per surface) —
720
- * the web app binds Redux; another client binds its own.
721
- */
722
-
723
714
  interface UseSourceSetOptions {
724
715
  /**
725
716
  * The draft store has been read back (a device store loads a moment after
@@ -47,7 +47,13 @@ interface SourceRef {
47
47
  promote?: ResourcePromotion | ResourcePromotion[];
48
48
  exclude?: string[];
49
49
  }
50
- interface SourceSet {
50
+ /**
51
+ * A TYPE, not an interface (2026-10-23): it IS a JSON object, so it is assignable wherever the server
52
+ * contract types the field as a JSON object (@ai-matrx/api's request bodies carry `source_set` as
53
+ * `{ [key: string]: JsonValue }` — the podcast generate request). An interface has no implicit index
54
+ * signature and would need a cast at every send.
55
+ */
56
+ type SourceSet = {
51
57
  __kind: typeof SOURCE_SET_KIND;
52
58
  version: typeof SOURCE_SET_VERSION;
53
59
  sources: SourceRef[];
@@ -59,7 +65,7 @@ interface SourceSet {
59
65
  retrieve_query?: string;
60
66
  /** The model whose context window sets the budget. */
61
67
  target_model_id?: string;
62
- }
68
+ };
63
69
  type SourceState = "ready" | "processing" | "failed" | "unavailable";
64
70
  interface SourceManifestForm {
65
71
  form: string;
@@ -705,21 +711,6 @@ interface FileRecoveryDeps {
705
711
  fileOrganizationId?: ((fileId: string) => string | null | undefined) | undefined;
706
712
  }
707
713
 
708
- /**
709
- * `@ai-matrx/agents/sources/react` — thin React hooks over the Source runtime
710
- * (`@ai-matrx/agents/sources/runtime`). No logic lives here that a non-React
711
- * client could not reach through the runtime itself: the hooks subscribe,
712
- * schedule, and hand back the same controller.
713
- *
714
- * const set = useSourceSet(adapter, { persistenceReady });
715
- * const intake = useSourceIntake(set, deps);
716
- * useSourceRecovery(set, intake, runner, { organizationId, readFileState });
717
- * const search = useSourcePartsSearch(client, ref, query);
718
- *
719
- * A host binds the adapter to its own store once (memoized per surface) —
720
- * the web app binds Redux; another client binds its own.
721
- */
722
-
723
714
  interface UseSourceSetOptions {
724
715
  /**
725
716
  * The draft store has been read back (a device store loads a moment after
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/agents",
3
- "version": "0.59.256",
3
+ "version": "0.59.258",
4
4
  "description": "Portable AI Matrx agent stream protocol, event projection, and safe result-presentation primitives.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "files": [
26
26
  "dist",
27
+ "scripts/check-model-catalog-reads.mjs",
27
28
  "README.md",
28
29
  "LICENSE",
29
30
  "CHANGELOG.md"
@@ -508,12 +509,12 @@
508
509
  },
509
510
  "sideEffects": false,
510
511
  "dependencies": {
511
- "@ai-matrx/api": "latest",
512
- "@ai-matrx/data": "latest",
513
512
  "@ai-matrx/alchemy": "latest",
513
+ "@ai-matrx/api": "latest",
514
+ "@ai-matrx/associations": "latest",
514
515
  "@ai-matrx/content-ir": "latest",
516
+ "@ai-matrx/data": "latest",
515
517
  "@ai-matrx/design-system": "latest",
516
- "@ai-matrx/associations": "latest",
517
518
  "@ai-matrx/diff": "latest",
518
519
  "@ai-matrx/icons": "latest",
519
520
  "@ai-matrx/kit": "latest"
@@ -555,11 +556,11 @@
555
556
  "scripts": {
556
557
  "build": "pnpm clean && node ./scripts/build-parts.mjs && node ../scripts/guard-exit.mjs ../scripts/check-node-env-not-inlined.mjs",
557
558
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
558
- "check:package": "node ./scripts/check-catalog-reader-columns.mjs && node ./scripts/check-catalog-reader-columns.mjs --self-test && node ../scripts/check-no-model-literals.mjs && node ../scripts/check-no-model-literals.mjs --self-test && node ../scripts/check-agents-below-chat.mjs && node ../scripts/check-agents-below-chat.mjs --self-test && pnpm build && node ../scripts/check-exports-agreement.mjs && publint && pnpm verify:tarball && node ../scripts/check-tarball-relative-imports.mjs --self-test && node ../scripts/check-tarball-relative-imports.mjs --pack . && node ../scripts/check-package-rules.mjs --self-test && node ../scripts/check-package-rules.mjs .",
559
+ "check:package": "node ./scripts/check-model-catalog-reads.mjs && node ./scripts/check-model-catalog-reads.mjs --self-test && node ./scripts/check-catalog-reader-columns.mjs && node ./scripts/check-catalog-reader-columns.mjs --self-test && node ../scripts/check-no-model-literals.mjs && node ../scripts/check-no-model-literals.mjs --self-test && node ../scripts/check-agents-below-chat.mjs && node ../scripts/check-agents-below-chat.mjs --self-test && pnpm build && node ../scripts/check-exports-agreement.mjs && publint && pnpm verify:tarball && node ../scripts/check-tarball-relative-imports.mjs --self-test && node ../scripts/check-tarball-relative-imports.mjs --pack . && node ../scripts/check-package-rules.mjs --self-test && node ../scripts/check-package-rules.mjs .",
559
560
  "typecheck": "tsc --noEmit",
560
561
  "test": "vitest run",
561
562
  "verify:tarball": "node ./scripts/verify-tarball.mjs",
562
563
  "typegen:self-test": "node scripts/typegen/drop-guard.self-test.mjs && node scripts/typegen/check-api-types-fresh.mjs --self-test"
563
564
  },
564
- "gitHead": "76b41cb76b379c9c04a1e26535fcf64606f59595"
565
+ "gitHead": "48dc0c53e766ad507fa37f41893b2b358be229f4"
565
566
  }
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ONE MODEL CATALOG READER (owner ruling 2026-10-23). Every surface that names or lists a model reads the
4
+ * model catalog through `@ai-matrx/agents/models` (records / readModelCatalog, subscribed to
5
+ * `catalog_changed` in models/catalog/changes.ts) and computes the name with `modelDisplayLabel`
6
+ * (models/label.ts), so an admin edit reaches every open app within seconds.
7
+ *
8
+ * Fails when a file outside apps/shared/matrx-agents/models/:
9
+ * R1 reads ai.model_definition / model_public / model_offering / model_config / model_variant /
10
+ * model_presentation (`.from("…")`) or the catalog RPCs (`admin_model_catalog`, `resolve_model_config`);
11
+ * R2 computes a model label by hand (`x.common_name || x.name`, `?? …name`) instead of `modelDisplayLabel`.
12
+ * ALLOWED lists the few readers that keep their own read on purpose, each with why and how it follows the
13
+ * event. Test files are fixtures and are not scanned.
14
+ *
15
+ * node apps/shared/matrx-agents/scripts/check-model-catalog-reads.mjs [--root <repo>] [--extra <dir>]... [--self-test]
16
+ * `--extra` adds a scan root (the red proof plants an untracked scratch file there). `--scan` replaces the
17
+ * default roots (Matrx 2: `node node_modules/@ai-matrx/agents/scripts/check-model-catalog-reads.mjs --root . --scan app`).
18
+ */
19
+ import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join, relative, resolve } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ const here = fileURLToPath(new URL(".", import.meta.url));
25
+ const args = process.argv.slice(2);
26
+ const flag = (name) => {
27
+ const out = [];
28
+ for (let i = 0; i < args.length; i++) if (args[i] === name && args[i + 1]) out.push(args[++i]);
29
+ return out;
30
+ };
31
+ const ROOT = resolve(flag("--root")[0] ?? join(here, "../../../.."));
32
+ // `--scan <dir>` replaces the aidream roots: Matrx 2 runs this file from its installed
33
+ // node_modules/@ai-matrx/agents with `--root . --scan app` (one guard, two repos).
34
+ const ROOTS = [...(flag("--scan").length ? flag("--scan") : ["apps/shared", "apps/matrx-frontend"]), ...flag("--extra")];
35
+
36
+ /** The reader module: the only place the catalog relations are read and the label is computed. */
37
+ const HOME = "apps/shared/matrx-agents/models/";
38
+
39
+ /** Files that keep their own catalog read, on purpose. Each says why and how it follows catalog_changed. */
40
+ export const ALLOWED = {
41
+ // The admin model EDITOR: reads and writes every column of the rows it edits. Every container that shows
42
+ // its rows re-reads on the event (useOnModelCatalogChanged / useModelCatalogRevision).
43
+ "apps/matrx-frontend/features/ai-models/service.ts": "admin editor CRUD; containers follow catalog_changed",
44
+ "apps/matrx-frontend/features/ai-models/discovery/api.ts": "admin discovery CRUD; UnifiedModelKindTable follows catalog_changed",
45
+ "apps/matrx-frontend/features/ai-models/translation/data.ts": "admin translation grid; settings-translation keys its query on the revision",
46
+ // cx-dashboard names models on the server per request; CxDashboardLayoutClient router.refresh()es on the event.
47
+ "apps/matrx-frontend/features/cx-dashboard/service.ts": "server-rendered admin page; layout refreshes on catalog_changed",
48
+ // The public JSON API for external callers (anon key, CDN cached by design) — not a screen.
49
+ "apps/matrx-frontend/app/api/ai-models/route.ts": "public CDN-cached JSON API for external callers, not a screen",
50
+ // A security probe that reads the relations to prove their grants.
51
+ "apps/matrx-frontend/scripts/check-rls-on.ts": "RLS probe, not a reader",
52
+ };
53
+
54
+ const SKIP_DIR = new Set(["node_modules", "dist", ".next", ".turbo", "coverage", "generated", "__tests__", "__fixtures__"]);
55
+ const isTest = (p) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p) || /\.d\.ts$/.test(p) || /\.generated\./.test(p);
56
+
57
+ const R1 = /\.from\(\s*["'`](model_definition|model_public|model_offering|model_config|model_variant|model_presentation)["'`]|\.rpc\(\s*["'`](admin_model_catalog|resolve_model_config)["'`]/g;
58
+ const R2 = /\bcommon_name\s*(\|\||\?\?)\s*[\w$?.]*\bname\b/g;
59
+
60
+ function* walk(dir) {
61
+ let names;
62
+ try {
63
+ names = readdirSync(dir);
64
+ } catch {
65
+ return;
66
+ }
67
+ for (const n of names) {
68
+ if (SKIP_DIR.has(n)) continue;
69
+ const p = join(dir, n);
70
+ const st = statSync(p);
71
+ if (st.isDirectory()) yield* walk(p);
72
+ else if (/\.(ts|tsx|mts|cts|js|jsx|mjs)$/.test(n) && !isTest(n)) yield p;
73
+ }
74
+ }
75
+
76
+ const lineOf = (src, index) => src.slice(0, index).split("\n").length;
77
+ function scan(roots) {
78
+ const findings = [];
79
+ for (const r of roots) {
80
+ for (const file of walk(resolve(ROOT, r))) {
81
+ const rel = relative(ROOT, file).split("\\").join("/");
82
+ if (rel.startsWith(HOME) || rel === "apps/shared/matrx-agents/scripts/check-model-catalog-reads.mjs") continue;
83
+ if (ALLOWED[rel]) continue;
84
+ const src = readFileSync(file, "utf8");
85
+ for (const [rule, re, what] of [
86
+ ["R1", R1, "reads the model catalog directly — use @ai-matrx/agents/models (records / chat's useModelLabel / readModelIdentities)"],
87
+ ["R2", R2, "computes a model label by hand — use modelDisplayLabel(row) from @ai-matrx/agents/models"],
88
+ ]) {
89
+ re.lastIndex = 0;
90
+ for (let m; (m = re.exec(src)); ) findings.push(`${rel}:${lineOf(src, m.index)} ${rule} ${what} [${m[0].replace(/\s+/g, " ")}]`);
91
+ }
92
+ }
93
+ }
94
+ return findings;
95
+ }
96
+
97
+ if (args.includes("--self-test")) {
98
+ // Falsifiability: a planted direct read and a hand-made label must each fail; the lawful form must pass.
99
+ const dir = mkdtempSync(join(tmpdir(), "model-catalog-reads-"));
100
+ try {
101
+ writeFileSync(join(dir, "bad.ts"), 'export const r = (db: any) => db.schema("ai").from("model_definition").select("id");\nexport const l = (m: any) =>\n m?.common_name ??\n m?.name;\n');
102
+ writeFileSync(join(dir, "good.ts"), 'import { modelDisplayLabel } from "@ai-matrx/agents/models";\nexport const l = (m: any) => modelDisplayLabel(m) ?? m.common_name ?? null;\n');
103
+ const got = scan([dir]);
104
+ const ok = got.length === 2 && got.some((f) => f.includes("bad.ts:1 R1")) && got.some((f) => f.includes("bad.ts:3 R2"));
105
+ if (!ok) {
106
+ console.error(`check-model-catalog-reads --self-test: FAILED — expected R1+R2 on bad.ts only, got:\n ${got.join("\n ") || "(nothing)"}`);
107
+ process.exit(1);
108
+ }
109
+ console.log("check-model-catalog-reads --self-test: OK — a planted read and a hand-made label both go red; modelDisplayLabel passes.");
110
+ process.exit(0);
111
+ } finally {
112
+ rmSync(dir, { recursive: true, force: true });
113
+ }
114
+ }
115
+
116
+ const findings = scan(ROOTS);
117
+ if (findings.length) {
118
+ console.error(`check-model-catalog-reads: ${findings.length} finding(s) — every model name and list goes through @ai-matrx/agents/models so catalog_changed reaches it:`);
119
+ for (const f of findings) console.error(` ${f}`);
120
+ process.exit(1);
121
+ }
122
+ console.log(`check-model-catalog-reads: OK — no model catalog read or hand-made model label outside ${HOME} (${Object.keys(ALLOWED).length} allowed readers).`);