@blinkk/root-cms 3.0.1-beta.3 → 3.0.1-beta.4

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.
@@ -1,10 +1,6 @@
1
1
  import {
2
- RootCMSClient,
3
2
  getCmsPlugin
4
- } from "./chunk-F4SODS5S.js";
5
-
6
- // core/cron.ts
7
- import { Timestamp as Timestamp3 } from "firebase-admin/firestore";
3
+ } from "./chunk-2BSW7SIH.js";
8
4
 
9
5
  // core/search-index.ts
10
6
  import fs from "fs";
@@ -677,8 +673,8 @@ var SearchIndexService = class {
677
673
  async listAllDocs(collectionIds) {
678
674
  const all = [];
679
675
  for (const collectionId of collectionIds) {
680
- const path3 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
681
- const snap = await this.db.collection(path3).get();
676
+ const path2 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
677
+ const snap = await this.db.collection(path2).get();
682
678
  snap.forEach((d) => {
683
679
  const docId = `${collectionId}/${d.id}`;
684
680
  if (!this.isDocIndexable(docId)) {
@@ -700,8 +696,8 @@ var SearchIndexService = class {
700
696
  async listChangedDocs(collectionIds, lastRun) {
701
697
  const all = [];
702
698
  for (const collectionId of collectionIds) {
703
- const path3 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
704
- const q = this.db.collection(path3).where("sys.modifiedAt", ">=", lastRun);
699
+ const path2 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
700
+ const q = this.db.collection(path2).where("sys.modifiedAt", ">=", lastRun);
705
701
  const snap = await q.get();
706
702
  snap.forEach((d) => {
707
703
  const docId = `${collectionId}/${d.id}`;
@@ -725,8 +721,8 @@ var SearchIndexService = class {
725
721
  async hasChangesSince(lastRun, docMap = null) {
726
722
  const collectionIds = await this.listCollectionIds();
727
723
  for (const collectionId of collectionIds) {
728
- const path3 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
729
- const q = this.db.collection(path3).where("sys.modifiedAt", ">=", lastRun).limit(1);
724
+ const path2 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
725
+ const q = this.db.collection(path2).where("sys.modifiedAt", ">=", lastRun).limit(1);
730
726
  const snap = await q.get();
731
727
  if (!snap.empty) {
732
728
  return true;
@@ -739,8 +735,8 @@ var SearchIndexService = class {
739
735
  }
740
736
  const liveIds = /* @__PURE__ */ new Set();
741
737
  for (const collectionId of collectionIds) {
742
- const path3 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
743
- const snap = await this.db.collection(path3).select().get();
738
+ const path2 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
739
+ const snap = await this.db.collection(path2).select().get();
744
740
  snap.forEach((d) => {
745
741
  const docId = `${collectionId}/${d.id}`;
746
742
  if (this.isDocIndexable(docId)) {
@@ -807,8 +803,8 @@ var SearchIndexService = class {
807
803
  );
808
804
  continue;
809
805
  }
810
- const path3 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
811
- const snap = await this.db.collection(path3).get();
806
+ const path2 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
807
+ const snap = await this.db.collection(path2).get();
812
808
  snap.forEach((d) => {
813
809
  const data = d.data() || {};
814
810
  const slug = data.slug || d.id;
@@ -884,8 +880,8 @@ var SearchIndexService = class {
884
880
  }
885
881
  const liveIds = /* @__PURE__ */ new Set();
886
882
  for (const collectionId of collectionIds) {
887
- const path3 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
888
- const snap = await this.db.collection(path3).select().get();
883
+ const path2 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
884
+ const snap = await this.db.collection(path2).select().get();
889
885
  snap.forEach((d) => {
890
886
  const docId = `${collectionId}/${d.id}`;
891
887
  if (this.isDocIndexable(docId)) {
@@ -1001,180 +997,6 @@ var SearchIndexService = class {
1001
997
  }
1002
998
  };
1003
999
 
1004
- // core/versions.ts
1005
- import fs2 from "fs";
1006
- import path2 from "path";
1007
- import { Timestamp as Timestamp2 } from "firebase-admin/firestore";
1008
- import glob2 from "tiny-glob";
1009
- var DOCUMENT_SAVE_OFFSET = 5 * 60 * 1e3;
1010
- var VersionsService = class {
1011
- constructor(rootConfig) {
1012
- this.rootConfig = rootConfig;
1013
- const cmsPlugin = getCmsPlugin(rootConfig);
1014
- const cmsPluginOptions = cmsPlugin.getConfig();
1015
- const projectId = cmsPluginOptions.id || "default";
1016
- this.projectId = projectId;
1017
- this.db = cmsPlugin.getFirestore();
1018
- }
1019
- /**
1020
- * Saves a version of all documents that have been edited since the last run.
1021
- */
1022
- async saveVersions() {
1023
- const lastRun = await this.getLastRun();
1024
- const lastRunWithOffset = lastRun === 0 ? lastRun : lastRun - DOCUMENT_SAVE_OFFSET;
1025
- const changedDocs = await this.getDocsModifiedAfter(lastRunWithOffset);
1026
- const now = Timestamp2.now().toMillis();
1027
- const versions = changedDocs.filter((doc) => {
1028
- const modifiedAt = doc.sys.modifiedAt.toMillis();
1029
- if (modifiedAt > now - DOCUMENT_SAVE_OFFSET) {
1030
- return false;
1031
- }
1032
- const publishedAt = doc.sys.publishedAt?.toMillis?.();
1033
- if (publishedAt && Math.abs(publishedAt - modifiedAt) < 5e3) {
1034
- return false;
1035
- }
1036
- return true;
1037
- });
1038
- if (versions.length > 0) {
1039
- this.saveVersionsToFirestore(versions);
1040
- }
1041
- this.saveLastRun(now);
1042
- }
1043
- async saveVersionsToFirestore(versions) {
1044
- const batch = this.db.batch();
1045
- versions.forEach((version) => {
1046
- if (!version.collection || !version.slug || !version.sys?.modifiedAt) {
1047
- return;
1048
- }
1049
- const modifiedAtMillis = version.sys.modifiedAt.toMillis();
1050
- const versionPath = `Projects/${this.projectId}/Collections/${version.collection}/Drafts/${version.slug}/Versions/${modifiedAtMillis}`;
1051
- console.log(versionPath);
1052
- const versionRef = this.db.doc(versionPath);
1053
- batch.set(versionRef, version);
1054
- });
1055
- await batch.commit();
1056
- console.log(`versions: saved ${versions.length} versions`);
1057
- }
1058
- /**
1059
- * Returns the last time (in millis) saveVersions() was run, or 0 if has
1060
- * never been run.
1061
- */
1062
- async getLastRun() {
1063
- const projectDocRef = this.db.collection("Projects").doc(this.projectId);
1064
- const projectDoc = await projectDocRef.get();
1065
- if (projectDoc.exists) {
1066
- const data = projectDoc.data() || {};
1067
- const ts = data.versionsLastRun;
1068
- if (ts) {
1069
- return ts.toMillis();
1070
- }
1071
- }
1072
- return 0;
1073
- }
1074
- /**
1075
- * Saves {versionLastRun: <timestamp>} to the Projects/<projectId> doc.
1076
- */
1077
- async saveLastRun(millis) {
1078
- const ts = Timestamp2.fromMillis(millis);
1079
- const projectDocRef = this.db.collection("Projects").doc(this.projectId);
1080
- await projectDocRef.set({ versionsLastRun: ts }, { merge: true });
1081
- }
1082
- /**
1083
- * Returns a list of all docs that were edited after a certain time.
1084
- */
1085
- async getDocsModifiedAfter(millis) {
1086
- const ts = Timestamp2.fromMillis(millis);
1087
- const results = [];
1088
- const collectionIds = await this.listCollections();
1089
- for (const collectionId of collectionIds) {
1090
- const collectionPath = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
1091
- const query = this.db.collection(collectionPath).where("sys.modifiedAt", ">=", ts);
1092
- const querySnapshot = await query.get();
1093
- querySnapshot.forEach((doc) => {
1094
- results.push(doc.data());
1095
- });
1096
- }
1097
- return results;
1098
- }
1099
- /**
1100
- * Returns a list of collection ids for the Root project.
1101
- */
1102
- async listCollections() {
1103
- const collectionsDir = path2.join(this.rootConfig.rootDir, "collections");
1104
- if (!fs2.existsSync(collectionsDir)) {
1105
- return [];
1106
- }
1107
- const collectionIds = [];
1108
- const collectionFileNames = await glob2("*.schema.ts", {
1109
- cwd: collectionsDir
1110
- });
1111
- collectionFileNames.forEach((filename) => {
1112
- collectionIds.push(filename.slice(0, -10));
1113
- });
1114
- return collectionIds;
1115
- }
1116
- };
1117
-
1118
- // core/cron.ts
1119
- var SEARCH_INDEX_MIN_INTERVAL_MS = 5 * 60 * 1e3;
1120
- async function runCronJobs(rootConfig, options = {}) {
1121
- await Promise.all([
1122
- runCronJob("publishScheduledDocs", runPublishScheduledDocs, rootConfig),
1123
- runCronJob(
1124
- "syncScheduledDataSources",
1125
- runSyncScheduledDataSources,
1126
- rootConfig
1127
- ),
1128
- runCronJob("saveVersions", runSaveVersions, rootConfig),
1129
- runCronJob(
1130
- "incrementalSearchIndex",
1131
- runIncrementalSearchIndex,
1132
- rootConfig,
1133
- options.loadSchema
1134
- )
1135
- ]);
1136
- }
1137
- async function runCronJob(name, fn, ...args) {
1138
- try {
1139
- await fn(...args);
1140
- } catch (err) {
1141
- console.log(`cron failed: ${name}`);
1142
- console.error(String(err.stack || err));
1143
- throw err;
1144
- }
1145
- }
1146
- async function runPublishScheduledDocs(rootConfig) {
1147
- const cmsClient = new RootCMSClient(rootConfig);
1148
- await cmsClient.publishScheduledDocs();
1149
- await cmsClient.publishScheduledReleases();
1150
- }
1151
- async function runSyncScheduledDataSources(rootConfig) {
1152
- const cmsClient = new RootCMSClient(rootConfig);
1153
- await cmsClient.syncScheduledDataSources();
1154
- }
1155
- async function runSaveVersions(rootConfig) {
1156
- const service = new VersionsService(rootConfig);
1157
- await service.saveVersions();
1158
- }
1159
- async function runIncrementalSearchIndex(rootConfig, loadSchema) {
1160
- const service = new SearchIndexService(rootConfig, loadSchema);
1161
- const status = await service.getStatus();
1162
- if (status.lastRun !== null) {
1163
- const elapsed = Date.now() - status.lastRun;
1164
- if (elapsed < SEARCH_INDEX_MIN_INTERVAL_MS) {
1165
- return;
1166
- }
1167
- const hasChanges = await service.hasChangesSince(
1168
- Timestamp3.fromMillis(status.lastRun)
1169
- );
1170
- if (!hasChanges) {
1171
- return;
1172
- }
1173
- }
1174
- await service.rebuildIndex({ force: false });
1175
- }
1176
-
1177
1000
  export {
1178
- SearchIndexService,
1179
- runCronJobs
1001
+ SearchIndexService
1180
1002
  };
@@ -1,3 +1,10 @@
1
+ import {
2
+ SearchIndexService
3
+ } from "./chunk-C245C557.js";
4
+ import {
5
+ normalizeSlug
6
+ } from "./chunk-2BSW7SIH.js";
7
+
1
8
  // core/ai.ts
2
9
  import crypto from "crypto";
3
10
  import { promises as fs } from "fs";
@@ -18,6 +25,40 @@ import { Timestamp } from "firebase-admin/firestore";
18
25
  // core/ai-tools.ts
19
26
  import { tool } from "ai";
20
27
  import { z } from "zod";
28
+
29
+ // shared/marshal.ts
30
+ function isArrayObject(data) {
31
+ return data !== null && typeof data === "object" && !Array.isArray(data) && Array.isArray(data._array);
32
+ }
33
+ function unmarshalData(data, options) {
34
+ if (data === null || data === void 0) {
35
+ return data;
36
+ }
37
+ if (typeof data !== "object") {
38
+ return data;
39
+ }
40
+ if (Array.isArray(data)) {
41
+ return data.map((item) => unmarshalData(item, options));
42
+ }
43
+ if (options?.isTimestamp?.(data)) {
44
+ return options.timestampToValue ? options.timestampToValue(data) : data;
45
+ }
46
+ if (typeof data.toMillis === "function") {
47
+ return data.toMillis();
48
+ }
49
+ if (isArrayObject(data)) {
50
+ return data._array.map(
51
+ (key) => unmarshalData(data[key] ?? {}, options)
52
+ );
53
+ }
54
+ const out = {};
55
+ for (const [k, v] of Object.entries(data)) {
56
+ out[k] = unmarshalData(v, options);
57
+ }
58
+ return out;
59
+ }
60
+
61
+ // core/ai-tools.ts
21
62
  var READ_ONLY_CMS_TOOL_NAMES = [
22
63
  "collections_list",
23
64
  "docs_list",
@@ -27,8 +68,8 @@ var READ_ONLY_CMS_TOOL_NAMES = [
27
68
  "doc_listVersions",
28
69
  "schema_get"
29
70
  ];
30
- function createReadOnlyCmsTools() {
31
- const all = createCmsTools();
71
+ function createReadOnlyCmsTools(ctx) {
72
+ const all = createCmsTools(ctx);
32
73
  const out = {};
33
74
  for (const name of READ_ONLY_CMS_TOOL_NAMES) {
34
75
  if (all[name]) {
@@ -37,11 +78,21 @@ function createReadOnlyCmsTools() {
37
78
  }
38
79
  return out;
39
80
  }
40
- function createCmsTools() {
81
+ function createCmsTools(ctx) {
41
82
  return {
42
83
  collections_list: tool({
43
84
  description: "List all CMS collections defined in the project. Returns each collection id along with optional name/description metadata.",
44
- inputSchema: z.object({})
85
+ inputSchema: z.object({}),
86
+ execute: async () => {
87
+ const collections = await ctx.loadAllCollections();
88
+ return {
89
+ collections: Object.entries(collections).map(([id, meta]) => ({
90
+ id,
91
+ name: meta.name,
92
+ description: meta.description
93
+ }))
94
+ };
95
+ }
45
96
  }),
46
97
  docs_list: tool({
47
98
  description: "List documents inside a CMS collection. Returns up to `limit` docs (default 25, max 100). Use this to explore content before reading individual docs.",
@@ -49,14 +100,32 @@ function createCmsTools() {
49
100
  collectionId: z.string().describe('Collection id, e.g. "Pages" or "BlogPosts".'),
50
101
  mode: z.enum(["draft", "published"]).default("draft").describe("Whether to read draft or published versions."),
51
102
  limit: z.number().int().min(1).max(100).default(25)
52
- })
103
+ }),
104
+ execute: async ({ collectionId, mode = "draft", limit = 25 }) => {
105
+ const max = clampInt(limit, 1, 100, 25);
106
+ const result = await ctx.cmsClient.listDocs(collectionId, {
107
+ mode,
108
+ limit: max
109
+ });
110
+ return {
111
+ docs: result.docs.map((d) => ({
112
+ id: d.id,
113
+ slug: d.slug,
114
+ sys: unmarshalSys(d.sys)
115
+ }))
116
+ };
117
+ }
53
118
  }),
54
119
  docs_search: tool({
55
120
  description: "Run a full-text search across all indexed CMS docs. Returns the top matching doc ids ordered by relevance.",
56
121
  inputSchema: z.object({
57
122
  query: z.string().min(1),
58
123
  limit: z.number().int().min(1).max(50).default(10)
59
- })
124
+ }),
125
+ execute: async ({ query, limit = 10 }) => {
126
+ const max = clampInt(limit, 1, 50, 10);
127
+ return await ctx.search(query, { limit: max });
128
+ }
60
129
  }),
61
130
  doc_get: tool({
62
131
  description: "Read a single CMS document. Returns the doc fields plus system metadata. Use this when you need the full content of a doc.",
@@ -65,7 +134,24 @@ function createCmsTools() {
65
134
  'Full doc id in the form "Collection/slug" (e.g. "Pages/home").'
66
135
  ),
67
136
  mode: z.enum(["draft", "published"]).default("draft")
68
- })
137
+ }),
138
+ execute: async ({ docId, mode = "draft" }) => {
139
+ const { collection, slug } = parseDocId(docId);
140
+ const raw = await ctx.cmsClient.getRawDoc(collection, slug, { mode });
141
+ if (!raw) {
142
+ return { found: false };
143
+ }
144
+ return {
145
+ found: true,
146
+ doc: {
147
+ id: raw.id,
148
+ collection: raw.collection,
149
+ slug: raw.slug,
150
+ sys: unmarshalSys(raw.sys),
151
+ fields: unmarshalFields(raw.fields)
152
+ }
153
+ };
154
+ }
69
155
  }),
70
156
  doc_getVersion: tool({
71
157
  description: 'Read a specific version of a CMS document. Use versionId "draft" or "published" for the current draft/published state, or a numeric timestamp for a historical version (from `doc_listVersions`).',
@@ -76,7 +162,33 @@ function createCmsTools() {
76
162
  versionId: z.string().describe(
77
163
  'Version identifier: "draft", "published", or a numeric timestamp.'
78
164
  )
79
- })
165
+ }),
166
+ execute: async ({ docId, versionId }) => {
167
+ const { collection, slug } = parseDocId(docId);
168
+ let path2;
169
+ if (versionId === "draft") {
170
+ path2 = `Projects/${ctx.cmsClient.projectId}/Collections/${collection}/Drafts/${slug}`;
171
+ } else if (versionId === "published") {
172
+ path2 = `Projects/${ctx.cmsClient.projectId}/Collections/${collection}/Published/${slug}`;
173
+ } else {
174
+ path2 = `Projects/${ctx.cmsClient.projectId}/Collections/${collection}/Drafts/${slug}/Versions/${versionId}`;
175
+ }
176
+ const snap = await ctx.cmsClient.db.doc(path2).get();
177
+ if (!snap.exists) {
178
+ return { found: false };
179
+ }
180
+ const raw = snap.data();
181
+ return {
182
+ found: true,
183
+ doc: {
184
+ id: raw.id,
185
+ collection: raw.collection,
186
+ slug: raw.slug,
187
+ sys: unmarshalSys(raw.sys),
188
+ fields: unmarshalFields(raw.fields)
189
+ }
190
+ };
191
+ }
80
192
  }),
81
193
  doc_set: tool({
82
194
  description: "Replace the entire draft fields payload of a CMS document. Pass the full JSON object that should become the new draft contents \u2014 any fields omitted will be removed. The payload is validated against the collection schema and the call is rejected on validation errors. Prefer `doc_updateField` for targeted edits. Only writes the draft version; users must publish separately. Always confirm with the user before calling.",
@@ -128,7 +240,24 @@ function createCmsTools() {
128
240
  'Full doc id in the form "Collection/slug" (e.g. "Pages/home").'
129
241
  ),
130
242
  limit: z.number().int().min(1).max(50).default(10)
131
- })
243
+ }),
244
+ execute: async ({ docId, limit = 10 }) => {
245
+ const { collection, slug } = parseDocId(docId);
246
+ const max = clampInt(limit, 1, 50, 10);
247
+ const path2 = `Projects/${ctx.cmsClient.projectId}/Collections/${collection}/Drafts/${slug}/Versions`;
248
+ const snap = await ctx.cmsClient.db.collection(path2).orderBy("sys.modifiedAt", "desc").limit(max).get();
249
+ return {
250
+ versions: snap.docs.map((d) => {
251
+ const data = d.data();
252
+ return {
253
+ versionId: d.id,
254
+ sys: unmarshalSys(data.sys),
255
+ tags: data.tags || [],
256
+ publishMessage: data.publishMessage
257
+ };
258
+ })
259
+ };
260
+ }
132
261
  }),
133
262
  doc_translateField: tool({
134
263
  description: "Translate a text value into one or more target locales using AI. Returns the translated strings keyed by locale. Use this to help with localization workflows.",
@@ -146,10 +275,63 @@ function createCmsTools() {
146
275
  description: "Get the field schema for a CMS collection. Returns the full field definitions including types, labels, and validation rules. Use this to understand what fields a collection supports before creating or editing docs.",
147
276
  inputSchema: z.object({
148
277
  collectionId: z.string().describe('Collection id, e.g. "Pages" or "BlogPosts".')
149
- })
278
+ }),
279
+ execute: async ({ collectionId }) => {
280
+ const schema = await ctx.loadCollection(collectionId);
281
+ if (!schema) {
282
+ return { found: false, collectionId };
283
+ }
284
+ return {
285
+ found: true,
286
+ collectionId,
287
+ fields: simplifyFields(schema.fields || [])
288
+ };
289
+ }
150
290
  })
151
291
  };
152
292
  }
293
+ function parseDocId(docId) {
294
+ const idx = docId.indexOf("/");
295
+ if (idx <= 0 || idx === docId.length - 1) {
296
+ throw new Error(`invalid docId: "${docId}" (expected "Collection/slug")`);
297
+ }
298
+ return {
299
+ collection: docId.slice(0, idx),
300
+ slug: normalizeSlug(docId.slice(idx + 1))
301
+ };
302
+ }
303
+ function clampInt(value, min, max, fallback) {
304
+ const n = Number.isFinite(value) ? Math.trunc(value) : fallback;
305
+ return Math.min(Math.max(n, min), max);
306
+ }
307
+ function unmarshalSys(sys) {
308
+ return unmarshalData(sys ?? {});
309
+ }
310
+ function unmarshalFields(fields) {
311
+ return unmarshalData(fields ?? {});
312
+ }
313
+ function simplifyFields(fields) {
314
+ return fields.map((f) => {
315
+ const out = { id: f.id, type: f.type };
316
+ if (f.label) out.label = f.label;
317
+ if (f.description) out.description = f.description;
318
+ if (f.required) out.required = true;
319
+ if (f.translate) out.translate = true;
320
+ if (f.options) out.options = f.options;
321
+ if (f.type === "object" && f.fields) {
322
+ out.fields = simplifyFields(f.fields);
323
+ }
324
+ if (f.type === "array" && f.of) {
325
+ out.of = simplifyFields([f.of])[0];
326
+ }
327
+ if (f.type === "oneof" && f.types) {
328
+ out.types = f.types.map(
329
+ (t) => typeof t === "string" ? t : { id: t.id, fields: simplifyFields(t.fields || []) }
330
+ );
331
+ }
332
+ return out;
333
+ });
334
+ }
153
335
 
154
336
  // core/ai.ts
155
337
  var ROOT_MD_FILENAME = "ROOT.md";
@@ -424,6 +606,25 @@ async function generateChatTitle(model, messages) {
424
606
  }
425
607
  return fallback;
426
608
  }
609
+ function buildCmsToolContext(options) {
610
+ let searchService = null;
611
+ return {
612
+ rootConfig: options.rootConfig,
613
+ cmsClient: options.cmsClient,
614
+ user: options.user,
615
+ loadCollection: options.loadCollection,
616
+ loadAllCollections: options.loadAllCollections,
617
+ search: async (query, opts) => {
618
+ if (!searchService) {
619
+ searchService = new SearchIndexService(
620
+ options.rootConfig,
621
+ options.loadCollection
622
+ );
623
+ }
624
+ return await searchService.search(query, opts);
625
+ }
626
+ };
627
+ }
427
628
  function buildExecutionModePrompt(mode) {
428
629
  const common = [
429
630
  "Root AI execution workflow:",
@@ -473,10 +674,19 @@ async function runChatStream(options) {
473
674
  cmsClient,
474
675
  user,
475
676
  chatId,
476
- executionMode = "approve"
677
+ executionMode = "approve",
678
+ loadCollection,
679
+ loadAllCollections
477
680
  } = options;
478
681
  const languageModel = resolveLanguageModel(model);
479
- const tools = model.capabilities?.tools === false ? {} : executionMode === "read" || executionMode === "suggest" ? createReadOnlyCmsTools() : createCmsTools();
682
+ const toolContext = buildCmsToolContext({
683
+ rootConfig,
684
+ cmsClient,
685
+ user,
686
+ loadCollection,
687
+ loadAllCollections
688
+ });
689
+ const tools = model.capabilities?.tools === false ? {} : executionMode === "read" || executionMode === "suggest" ? createReadOnlyCmsTools(toolContext) : createCmsTools(toolContext);
480
690
  const basePrompt = config.systemPrompt || [
481
691
  "You are an assistant embedded in the Root CMS admin UI.",
482
692
  "Help the user explore and edit content, answer questions about",
@@ -519,9 +729,26 @@ async function runChatStream(options) {
519
729
  });
520
730
  }
521
731
  async function runEditObjectStream(options) {
522
- const { rootConfig, model, config, messages, editData } = options;
732
+ const {
733
+ rootConfig,
734
+ cmsClient,
735
+ user,
736
+ model,
737
+ config,
738
+ messages,
739
+ editData,
740
+ loadCollection,
741
+ loadAllCollections
742
+ } = options;
523
743
  const languageModel = resolveLanguageModel(model);
524
- const tools = model.capabilities?.tools === false ? {} : createReadOnlyCmsTools();
744
+ const toolContext = buildCmsToolContext({
745
+ rootConfig,
746
+ cmsClient,
747
+ user,
748
+ loadCollection,
749
+ loadAllCollections
750
+ });
751
+ const tools = model.capabilities?.tools === false ? {} : createReadOnlyCmsTools(toolContext);
525
752
  const editPromptText = (await import("./edit-XX3LAGK6.js")).default;
526
753
  const promptParts = [
527
754
  stripLegacyEditOutputSpec(editPromptText),
@@ -776,7 +1003,7 @@ function extractJsonFromResponse(responseText) {
776
1003
  // package.json
777
1004
  var package_default = {
778
1005
  name: "@blinkk/root-cms",
779
- version: "3.0.1-beta.3",
1006
+ version: "3.0.1-beta.4",
780
1007
  author: "s@blinkk.com",
781
1008
  license: "MIT",
782
1009
  engines: {
@@ -945,7 +1172,7 @@ var package_default = {
945
1172
  yjs: "13.6.27"
946
1173
  },
947
1174
  peerDependencies: {
948
- "@blinkk/root": "3.0.1-beta.3",
1175
+ "@blinkk/root": "3.0.1-beta.4",
949
1176
  "firebase-admin": ">=11",
950
1177
  "firebase-functions": ">=4"
951
1178
  },
package/dist/cli.js CHANGED
@@ -5,8 +5,7 @@ import {
5
5
  RootCMSClient,
6
6
  getCmsPlugin,
7
7
  unmarshalData
8
- } from "./chunk-F4SODS5S.js";
9
- import "./chunk-CRK7N6RR.js";
8
+ } from "./chunk-2BSW7SIH.js";
10
9
  import "./chunk-MLKGABMK.js";
11
10
 
12
11
  // cli/cli.ts
package/dist/client.js CHANGED
@@ -12,8 +12,7 @@ import {
12
12
  translationsForLocale,
13
13
  unmarshalArray,
14
14
  unmarshalData
15
- } from "./chunk-F4SODS5S.js";
16
- import "./chunk-CRK7N6RR.js";
15
+ } from "./chunk-2BSW7SIH.js";
17
16
  import "./chunk-MLKGABMK.js";
18
17
  export {
19
18
  BatchRequest,
package/dist/core.js CHANGED
@@ -16,8 +16,7 @@ import {
16
16
  translationsForLocale,
17
17
  unmarshalArray,
18
18
  unmarshalData
19
- } from "./chunk-F4SODS5S.js";
20
- import "./chunk-CRK7N6RR.js";
19
+ } from "./chunk-2BSW7SIH.js";
21
20
  import {
22
21
  __export
23
22
  } from "./chunk-MLKGABMK.js";
package/dist/functions.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  runCronJobs
3
- } from "./chunk-TRM4MQHU.js";
4
- import "./chunk-F4SODS5S.js";
5
- import "./chunk-CRK7N6RR.js";
3
+ } from "./chunk-BGTUWIV6.js";
4
+ import "./chunk-C245C557.js";
5
+ import "./chunk-2BSW7SIH.js";
6
6
  import "./chunk-MLKGABMK.js";
7
7
 
8
8
  // core/functions.ts