@x12i/catalox 3.4.0 → 3.4.5

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 (33) hide show
  1. package/README.md +53 -0
  2. package/dist/src/catalox/ai-functions-loader.d.ts +6 -0
  3. package/dist/src/catalox/ai-functions-loader.d.ts.map +1 -0
  4. package/dist/src/catalox/ai-functions-loader.js +19 -0
  5. package/dist/src/catalox/ai-functions-loader.js.map +1 -0
  6. package/dist/src/catalox/catalog-discovery.d.ts +32 -0
  7. package/dist/src/catalox/catalog-discovery.d.ts.map +1 -0
  8. package/dist/src/catalox/catalog-discovery.js +150 -0
  9. package/dist/src/catalox/catalog-discovery.js.map +1 -0
  10. package/dist/src/catalox/catalox-bound.d.ts +8 -1
  11. package/dist/src/catalox/catalox-bound.d.ts.map +1 -1
  12. package/dist/src/catalox/catalox-bound.js +21 -0
  13. package/dist/src/catalox/catalox-bound.js.map +1 -1
  14. package/dist/src/catalox/catalox.d.ts +12 -1
  15. package/dist/src/catalox/catalox.d.ts.map +1 -1
  16. package/dist/src/catalox/catalox.js +220 -0
  17. package/dist/src/catalox/catalox.js.map +1 -1
  18. package/dist/src/contracts/descriptor-rules.d.ts +76 -0
  19. package/dist/src/contracts/descriptor-rules.d.ts.map +1 -0
  20. package/dist/src/contracts/descriptor-rules.js +2 -0
  21. package/dist/src/contracts/descriptor-rules.js.map +1 -0
  22. package/dist/src/contracts/descriptors.d.ts +5 -0
  23. package/dist/src/contracts/descriptors.d.ts.map +1 -1
  24. package/dist/src/contracts/discovery.d.ts +70 -0
  25. package/dist/src/contracts/discovery.d.ts.map +1 -1
  26. package/dist/src/contracts/index.d.ts +3 -1
  27. package/dist/src/contracts/index.d.ts.map +1 -1
  28. package/dist/src/contracts/index.js.map +1 -1
  29. package/dist/src/contracts/search.d.ts +127 -0
  30. package/dist/src/contracts/search.d.ts.map +1 -0
  31. package/dist/src/contracts/search.js +2 -0
  32. package/dist/src/contracts/search.js.map +1 -0
  33. package/package.json +2 -1
@@ -28,6 +28,8 @@ import { SnapshotStore } from "../firebase/snapshot-store.js";
28
28
  import { StoreAppBindingStore } from "../firebase/store-app-binding-store.js";
29
29
  import { IdentityBindingStore } from "../firebase/identity-binding-store.js";
30
30
  import { nativeItemsCollectionId } from "../firebase/catalog-data-paths.js";
31
+ import { applyCatalogVisibilityFilter, filterCatalogsByText, findCatalogsByAi as findCatalogsByAiHelper, summarizeCatalog, } from "./catalog-discovery.js";
32
+ import { loadAiFunctions } from "./ai-functions-loader.js";
31
33
  import { runBackupData, pruneGcsBackupRuns } from "./backup-data.js";
32
34
  import { runUndoFirestoreRestore } from "./restore-firestore-backup.js";
33
35
  import { runRestoreFirestoreBackupFromGcs, runDeleteCataloxGcsBackupRun } from "./restore-firestore-backup-from-gcs.js";
@@ -517,6 +519,33 @@ export class Catalox {
517
519
  }
518
520
  return out.filter((e) => (input.includeHidden ? true : e.visibility !== "hidden"));
519
521
  }
522
+ /**
523
+ * App-agnostic discovery: list catalog metadata across all apps.
524
+ * Visibility default: hide hidden catalogs unless `context.superAdmin`.
525
+ */
526
+ async listAllCatalogs(context, input) {
527
+ const catalogs = await this.deps.catalogs.listAll();
528
+ const out = [];
529
+ for (const c of catalogs) {
530
+ const d = await this.deps.descriptors.get(c.catalogId);
531
+ out.push(summarizeCatalog(c, d));
532
+ }
533
+ return applyCatalogVisibilityFilter(out, context, input);
534
+ }
535
+ async findCatalogs(context, input) {
536
+ const all = await this.listAllCatalogs(context, {
537
+ ...(input.includeDisabled != null ? { includeDisabled: input.includeDisabled } : {}),
538
+ ...(input.includeHidden != null ? { includeHidden: input.includeHidden } : {}),
539
+ });
540
+ return filterCatalogsByText(all, input);
541
+ }
542
+ async findCatalogsByAi(context, input) {
543
+ const all = await this.listAllCatalogs(context, {
544
+ ...(input.includeDisabled != null ? { includeDisabled: input.includeDisabled } : {}),
545
+ ...(input.includeHidden != null ? { includeHidden: input.includeHidden } : {}),
546
+ });
547
+ return findCatalogsByAiHelper({ query: input.query, catalogs: all, input });
548
+ }
520
549
  async getCatalogDescriptor(context, catalogId) {
521
550
  await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "read");
522
551
  const rec = await this.deps.descriptors.get(catalogId);
@@ -891,6 +920,197 @@ export class Catalox {
891
920
  async listCatalogs(_context, _options) {
892
921
  return this.deps.catalogs.listAll();
893
922
  }
923
+ async searchCatalogItems(context, catalogId, input) {
924
+ const poolLimit = input.poolLimit ?? 200;
925
+ const limit = input.limit ?? 50;
926
+ const base = await this.listCatalogItems(context, catalogId, {
927
+ limit: poolLimit,
928
+ ...(input.scope ? { scope: input.scope } : {}),
929
+ ...(input.filter ? { filter: input.filter } : {}),
930
+ ...(input.sort ? { sort: input.sort } : {}),
931
+ });
932
+ if (base.listOutcome !== "ok")
933
+ return base;
934
+ const q = String(input.text ?? "").toLowerCase().trim();
935
+ if (!q)
936
+ return { ...base, items: [] };
937
+ const textFields = input.textFields ?? [];
938
+ const filtered = base.items.filter((it) => {
939
+ const parts = [];
940
+ if (it.title)
941
+ parts.push(String(it.title));
942
+ if (it.subtitle)
943
+ parts.push(String(it.subtitle));
944
+ const data = it.data;
945
+ for (const p of textFields) {
946
+ const v = this.readPath(data, p);
947
+ if (v == null)
948
+ continue;
949
+ parts.push(typeof v === "string" || typeof v === "number" ? String(v) : JSON.stringify(v));
950
+ }
951
+ return parts.join(" ").toLowerCase().includes(q);
952
+ });
953
+ return { ...base, items: filtered.slice(0, limit) };
954
+ }
955
+ async findCatalogItemsByAi(context, catalogId, input) {
956
+ const poolLimit = input.poolLimit ?? 300;
957
+ const base = await this.listCatalogItems(context, catalogId, {
958
+ limit: poolLimit,
959
+ ...(input.scope ? { scope: input.scope } : {}),
960
+ ...(input.filter ? { filter: input.filter } : {}),
961
+ ...(input.sort ? { sort: input.sort } : {}),
962
+ });
963
+ if (base.listOutcome !== "ok")
964
+ return { query: input.query, hits: [], noMatch: true };
965
+ const items = base.items;
966
+ const candidates = items.map((it) => {
967
+ const title = it.title ? String(it.title) : "";
968
+ const subtitle = it.subtitle ? String(it.subtitle) : "";
969
+ const label = [title, subtitle].filter(Boolean).join(" — ") || String(it.itemId);
970
+ return {
971
+ id: String(it.itemId),
972
+ label,
973
+ metadata: { catalogId: String(catalogId) },
974
+ };
975
+ });
976
+ const { match } = await loadAiFunctions();
977
+ const res = await match({
978
+ query: input.query,
979
+ candidates,
980
+ ...(input.guidance ? { guidance: input.guidance } : {}),
981
+ ...(input.maxResults != null ? { maxResults: input.maxResults } : {}),
982
+ ...(input.minScore != null ? { minScore: input.minScore } : {}),
983
+ ...(input.allowNoMatch != null ? { allowNoMatch: input.allowNoMatch } : {}),
984
+ ...(input.returnReasons != null ? { returnReasons: input.returnReasons } : {}),
985
+ ...(input.additionalInstructions ? { additionalInstructions: input.additionalInstructions } : {}),
986
+ ...(input.maxCandidates != null ? { maxCandidates: input.maxCandidates } : {}),
987
+ ...(input.mode ? { mode: input.mode } : {}),
988
+ ...(input.client ? { client: input.client } : {}),
989
+ ...(input.model ? { model: input.model } : {}),
990
+ ...(input.temperature != null ? { temperature: input.temperature } : {}),
991
+ ...(input.maxTokens != null ? { maxTokens: input.maxTokens } : {}),
992
+ ...(input.timeoutMs != null ? { timeoutMs: input.timeoutMs } : {}),
993
+ ...(input.vendor != null ? { vendor: input.vendor } : {}),
994
+ });
995
+ const byId = new Map(items.map((it) => [String(it.itemId), it]));
996
+ const hits = res.matches
997
+ .map((m) => {
998
+ const it = byId.get(String(m.id));
999
+ if (!it)
1000
+ return null;
1001
+ return { itemId: it.itemId, score: m.score, ...(m.reason ? { reason: m.reason } : {}), item: it };
1002
+ })
1003
+ .filter(Boolean);
1004
+ return { query: input.query, hits, noMatch: Boolean(res.noMatch) };
1005
+ }
1006
+ async createCatalogItemByAi(context, catalogId, input) {
1007
+ await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "write");
1008
+ const descRec = await this.deps.descriptors.get(catalogId);
1009
+ if (!descRec)
1010
+ throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
1011
+ const rules = descRec.descriptor.creationRules;
1012
+ if (!rules)
1013
+ throw new CatalogAdapterError({ catalogId, reason: "creation_rules_missing" });
1014
+ if (rules.enabled === false)
1015
+ throw new CatalogAdapterError({ catalogId, reason: "creation_rules_disabled" });
1016
+ const catalog = await this.deps.catalogs.get(catalogId);
1017
+ if (!catalog)
1018
+ throw new CatalogNotFoundError({ catalogId });
1019
+ const existingLimit = input.existingItemsSampleLimit ?? 0;
1020
+ const existingItemsSample = existingLimit > 0
1021
+ ? (await this.listCatalogItems(context, catalogId, { limit: existingLimit })).items.map((i) => i.data)
1022
+ : undefined;
1023
+ const { createItem } = await loadAiFunctions();
1024
+ const result = await createItem({
1025
+ ...(descRec.descriptor.itemLabel ? { itemLabel: descRec.descriptor.itemLabel } : {}),
1026
+ creationRules: rules,
1027
+ provided: input.provided,
1028
+ fieldSchema: (descRec.descriptor.queryableFields ?? []),
1029
+ ...(existingItemsSample ? { existingItemsSample } : {}),
1030
+ ...(input.additionalInstructions ? { additionalInstructions: input.additionalInstructions } : {}),
1031
+ ...(input.mode ? { mode: input.mode } : {}),
1032
+ ...(input.model ? { model: input.model } : {}),
1033
+ ...(input.client ? { client: input.client } : {}),
1034
+ ...(input.temperature != null ? { temperature: input.temperature } : {}),
1035
+ ...(input.maxTokens != null ? { maxTokens: input.maxTokens } : {}),
1036
+ ...(input.timeoutMs != null ? { timeoutMs: input.timeoutMs } : {}),
1037
+ ...(input.vendor != null ? { vendor: input.vendor } : {}),
1038
+ }, undefined);
1039
+ const out = {
1040
+ catalogId,
1041
+ proposed: result.item ?? {},
1042
+ issues: (result.issues ?? []),
1043
+ ...(result.missingInputs ? { missingInputs: result.missingInputs } : {}),
1044
+ noCreate: Boolean(result.noCreate),
1045
+ ...(result.reason ? { reason: result.reason } : {}),
1046
+ };
1047
+ const autoPersist = input.autoPersist === true;
1048
+ const hasErrors = (result.issues ?? []).some((i) => i?.severity === "error");
1049
+ if (autoPersist && !out.noCreate && !hasErrors) {
1050
+ if (catalog.sourceMode !== "native") {
1051
+ throw new CatalogAdapterError({ catalogId, reason: "autopersist_mapped_unsupported" });
1052
+ }
1053
+ const writePayload = input.scope != null ? { ...out.proposed, scope: input.scope } : out.proposed;
1054
+ out.item = await this.upsertNativeCatalogItem(context, catalogId, writePayload);
1055
+ }
1056
+ return out;
1057
+ }
1058
+ async modifyCatalogItemByAi(context, catalogId, itemId, input) {
1059
+ await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "write");
1060
+ const descRec = await this.deps.descriptors.get(catalogId);
1061
+ if (!descRec)
1062
+ throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
1063
+ const rules = descRec.descriptor.modificationRules;
1064
+ if (!rules)
1065
+ throw new CatalogAdapterError({ catalogId, reason: "modification_rules_missing" });
1066
+ if (rules.enabled === false)
1067
+ throw new CatalogAdapterError({ catalogId, reason: "modification_rules_disabled" });
1068
+ const catalog = await this.deps.catalogs.get(catalogId);
1069
+ if (!catalog)
1070
+ throw new CatalogNotFoundError({ catalogId });
1071
+ const got = await this.getCatalogItem(context, catalogId, itemId, input.nativeItemOptions);
1072
+ if (got.outcome === "mapping_blocked") {
1073
+ throw new CatalogAdapterError({ catalogId, reason: "mapping_validation_failed", issues: got.issues });
1074
+ }
1075
+ if (got.outcome === "not_found")
1076
+ throw new CatalogNotFoundError({ catalogId, itemId });
1077
+ const currentItem = (got.item.data ?? {});
1078
+ const { modifyItem } = await loadAiFunctions();
1079
+ const result = await modifyItem({
1080
+ ...(descRec.descriptor.itemLabel ? { itemLabel: descRec.descriptor.itemLabel } : {}),
1081
+ currentItem: currentItem,
1082
+ modificationRules: rules,
1083
+ patch: input.patch,
1084
+ fieldSchema: (descRec.descriptor.queryableFields ?? []),
1085
+ ...(input.additionalInstructions ? { additionalInstructions: input.additionalInstructions } : {}),
1086
+ ...(input.mode ? { mode: input.mode } : {}),
1087
+ ...(input.model ? { model: input.model } : {}),
1088
+ ...(input.client ? { client: input.client } : {}),
1089
+ ...(input.temperature != null ? { temperature: input.temperature } : {}),
1090
+ ...(input.maxTokens != null ? { maxTokens: input.maxTokens } : {}),
1091
+ ...(input.timeoutMs != null ? { timeoutMs: input.timeoutMs } : {}),
1092
+ ...(input.vendor != null ? { vendor: input.vendor } : {}),
1093
+ }, undefined);
1094
+ const out = {
1095
+ catalogId,
1096
+ itemId,
1097
+ proposed: result.item ?? {},
1098
+ diff: (result.diff ?? []),
1099
+ issues: (result.issues ?? []),
1100
+ ...(result.violatedRules ? { violatedRules: result.violatedRules } : {}),
1101
+ noChange: Boolean(result.noChange),
1102
+ ...(result.reason ? { reason: result.reason } : {}),
1103
+ };
1104
+ const autoPersist = input.autoPersist === true;
1105
+ const hasErrors = (result.issues ?? []).some((i) => i?.severity === "error");
1106
+ if (autoPersist && !out.noChange && !hasErrors) {
1107
+ if (catalog.sourceMode !== "native") {
1108
+ throw new CatalogAdapterError({ catalogId, reason: "autopersist_mapped_unsupported" });
1109
+ }
1110
+ out.item = await this.updateNativeCatalogItem(context, catalogId, itemId, out.proposed, input.nativeItemOptions);
1111
+ }
1112
+ return out;
1113
+ }
894
1114
  async bindCatalogToApp(_context, _input) {
895
1115
  const existing = await this.deps.bindings.findByAppCatalog(_input.appId, _input.catalogId);
896
1116
  if (existing)