@blinkk/root-cms 3.0.1-beta.3 → 3.0.1-beta.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.
- package/dist/app.js +3 -2
- package/dist/{chunk-F4SODS5S.js → chunk-2BSW7SIH.js} +295 -4
- package/dist/{chunk-NXEXOHBD.js → chunk-5CZBPART.js} +243 -16
- package/dist/chunk-BGTUWIV6.js +187 -0
- package/dist/{chunk-TRM4MQHU.js → chunk-C245C557.js} +14 -192
- package/dist/cli.js +1 -2
- package/dist/client.js +1 -2
- package/dist/core.js +1 -2
- package/dist/functions.js +3 -3
- package/dist/plugin.js +29 -12
- package/dist/ui/ui.css +1 -1
- package/dist/ui/ui.js +166 -166
- package/package.json +3 -3
- package/dist/chunk-CRK7N6RR.js +0 -298
package/dist/app.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getServerVersion,
|
|
3
3
|
serializeAiConfig
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-5CZBPART.js";
|
|
5
|
+
import "./chunk-C245C557.js";
|
|
6
|
+
import "./chunk-2BSW7SIH.js";
|
|
6
7
|
import {
|
|
7
8
|
getCollectionSchema,
|
|
8
9
|
getProjectSchemas
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
validateFields
|
|
3
|
-
} from "./chunk-CRK7N6RR.js";
|
|
4
|
-
|
|
5
1
|
// core/client.ts
|
|
6
2
|
import crypto from "crypto";
|
|
7
3
|
import {
|
|
@@ -362,6 +358,301 @@ function buildTranslationsLocaleDocDbPath(options) {
|
|
|
362
358
|
).replace("{mode}", options.mode).replace("{id}", normalizeSlug(options.id)).replace("{locale}", options.locale);
|
|
363
359
|
}
|
|
364
360
|
|
|
361
|
+
// core/validation.ts
|
|
362
|
+
function validateFields(fieldsData, schema) {
|
|
363
|
+
if (fieldsData === null || fieldsData === void 0) {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
if (typeof fieldsData !== "object" || Array.isArray(fieldsData)) {
|
|
367
|
+
return [
|
|
368
|
+
{
|
|
369
|
+
path: "",
|
|
370
|
+
message: "Expected object for fields data",
|
|
371
|
+
expected: "object",
|
|
372
|
+
received: getType(fieldsData)
|
|
373
|
+
}
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
const errors = [];
|
|
377
|
+
for (const field of schema.fields) {
|
|
378
|
+
if (!field.id) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (!(field.id in fieldsData)) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const value = fieldsData[field.id];
|
|
385
|
+
errors.push(...validateValue(value, field, field.id));
|
|
386
|
+
}
|
|
387
|
+
return errors;
|
|
388
|
+
}
|
|
389
|
+
function validateValue(value, field, path) {
|
|
390
|
+
if (value === void 0) {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
switch (field.type) {
|
|
394
|
+
case "string":
|
|
395
|
+
case "select":
|
|
396
|
+
if (typeof value !== "string") {
|
|
397
|
+
return [createError(path, "string", value)];
|
|
398
|
+
}
|
|
399
|
+
return [];
|
|
400
|
+
case "number":
|
|
401
|
+
if (typeof value !== "number") {
|
|
402
|
+
return [createError(path, "number", value)];
|
|
403
|
+
}
|
|
404
|
+
if (isNaN(value)) {
|
|
405
|
+
return [createError(path, "number", value)];
|
|
406
|
+
}
|
|
407
|
+
return [];
|
|
408
|
+
case "boolean":
|
|
409
|
+
if (typeof value !== "boolean") {
|
|
410
|
+
return [createError(path, "boolean", value)];
|
|
411
|
+
}
|
|
412
|
+
return [];
|
|
413
|
+
case "date":
|
|
414
|
+
case "datetime": {
|
|
415
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
416
|
+
return [createError(path, "object", value)];
|
|
417
|
+
}
|
|
418
|
+
const errors = [];
|
|
419
|
+
const seconds = value.seconds;
|
|
420
|
+
const nanoseconds = value.nanoseconds;
|
|
421
|
+
if (seconds === void 0) {
|
|
422
|
+
errors.push({
|
|
423
|
+
path: `${path}.seconds`,
|
|
424
|
+
message: "Required",
|
|
425
|
+
expected: "number",
|
|
426
|
+
received: "undefined"
|
|
427
|
+
});
|
|
428
|
+
} else if (typeof seconds !== "number") {
|
|
429
|
+
errors.push(createError(`${path}.seconds`, "number", seconds));
|
|
430
|
+
}
|
|
431
|
+
if (nanoseconds === void 0) {
|
|
432
|
+
errors.push({
|
|
433
|
+
path: `${path}.nanoseconds`,
|
|
434
|
+
message: "Required",
|
|
435
|
+
expected: "number",
|
|
436
|
+
received: "undefined"
|
|
437
|
+
});
|
|
438
|
+
} else if (typeof nanoseconds !== "number") {
|
|
439
|
+
errors.push(createError(`${path}.nanoseconds`, "number", nanoseconds));
|
|
440
|
+
}
|
|
441
|
+
return errors;
|
|
442
|
+
}
|
|
443
|
+
case "multiselect":
|
|
444
|
+
if (!Array.isArray(value)) {
|
|
445
|
+
return [createError(path, "array", value)];
|
|
446
|
+
}
|
|
447
|
+
return value.flatMap((item, index) => {
|
|
448
|
+
if (typeof item !== "string") {
|
|
449
|
+
return [createError(`${path}.${index}`, "string", item)];
|
|
450
|
+
}
|
|
451
|
+
return [];
|
|
452
|
+
});
|
|
453
|
+
case "image":
|
|
454
|
+
case "file": {
|
|
455
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
456
|
+
return [createError(path, "object", value)];
|
|
457
|
+
}
|
|
458
|
+
const errors = [];
|
|
459
|
+
if (value.src === void 0) {
|
|
460
|
+
errors.push({
|
|
461
|
+
path: `${path}.src`,
|
|
462
|
+
message: "Required",
|
|
463
|
+
expected: "string",
|
|
464
|
+
received: "undefined"
|
|
465
|
+
});
|
|
466
|
+
} else if (typeof value.src !== "string") {
|
|
467
|
+
errors.push(createError(`${path}.src`, "string", value.src));
|
|
468
|
+
}
|
|
469
|
+
if (value.alt !== void 0 && typeof value.alt !== "string") {
|
|
470
|
+
errors.push(createError(`${path}.alt`, "string", value.alt));
|
|
471
|
+
}
|
|
472
|
+
return errors;
|
|
473
|
+
}
|
|
474
|
+
case "object": {
|
|
475
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
476
|
+
return [createError(path, "object", value)];
|
|
477
|
+
}
|
|
478
|
+
const objectField = field;
|
|
479
|
+
const errors = [];
|
|
480
|
+
for (const nestedField of objectField.fields) {
|
|
481
|
+
if (!nestedField.id || !(nestedField.id in value)) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
errors.push(
|
|
485
|
+
...validateValue(
|
|
486
|
+
value[nestedField.id],
|
|
487
|
+
nestedField,
|
|
488
|
+
`${path}.${nestedField.id}`
|
|
489
|
+
)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return errors;
|
|
493
|
+
}
|
|
494
|
+
case "array": {
|
|
495
|
+
if (!Array.isArray(value)) {
|
|
496
|
+
return [createError(path, "array", value)];
|
|
497
|
+
}
|
|
498
|
+
const arrayField = field;
|
|
499
|
+
const itemField = arrayField.of;
|
|
500
|
+
return value.flatMap((item, index) => {
|
|
501
|
+
return validateValue(item, itemField, `${path}.${index}`);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
case "oneof": {
|
|
505
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
506
|
+
return [createError(path, "object", value)];
|
|
507
|
+
}
|
|
508
|
+
const oneOfField = field;
|
|
509
|
+
const typeName = value._type;
|
|
510
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
511
|
+
const typeNames = [];
|
|
512
|
+
for (const type of oneOfField.types) {
|
|
513
|
+
if (typeof type === "string") {
|
|
514
|
+
typeNames.push(type);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
typeMap.set(type.name, type);
|
|
518
|
+
typeNames.push(type.name);
|
|
519
|
+
}
|
|
520
|
+
if (!typeNames.includes(typeName)) {
|
|
521
|
+
const expectedStr = typeNames.map((t) => `'${t}'`).join(" | ");
|
|
522
|
+
return [
|
|
523
|
+
{
|
|
524
|
+
path: `${path}._type`,
|
|
525
|
+
message: `Invalid discriminator value. Expected ${expectedStr}`,
|
|
526
|
+
expected: "valid discriminator value",
|
|
527
|
+
received: typeName
|
|
528
|
+
}
|
|
529
|
+
];
|
|
530
|
+
}
|
|
531
|
+
const matchedSchema = typeMap.get(typeName);
|
|
532
|
+
if (!matchedSchema) {
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
const errors = [];
|
|
536
|
+
for (const nestedField of matchedSchema.fields) {
|
|
537
|
+
if (!nestedField.id || !(nestedField.id in value)) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
errors.push(
|
|
541
|
+
...validateValue(
|
|
542
|
+
value[nestedField.id],
|
|
543
|
+
nestedField,
|
|
544
|
+
`${path}.${nestedField.id}`
|
|
545
|
+
)
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return errors;
|
|
549
|
+
}
|
|
550
|
+
case "richtext": {
|
|
551
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
552
|
+
return [createError(path, "object", value)];
|
|
553
|
+
}
|
|
554
|
+
const errors = [];
|
|
555
|
+
if (value.blocks === void 0) {
|
|
556
|
+
errors.push({
|
|
557
|
+
path: `${path}.blocks`,
|
|
558
|
+
message: "Required",
|
|
559
|
+
expected: "array",
|
|
560
|
+
received: "undefined"
|
|
561
|
+
});
|
|
562
|
+
} else if (!Array.isArray(value.blocks)) {
|
|
563
|
+
errors.push(createError(`${path}.blocks`, "array", value.blocks));
|
|
564
|
+
} else {
|
|
565
|
+
value.blocks.forEach((block, index) => {
|
|
566
|
+
if (typeof block !== "object" || block === null) {
|
|
567
|
+
errors.push(
|
|
568
|
+
createError(`${path}.blocks.${index}`, "object", block)
|
|
569
|
+
);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (block.type === void 0) {
|
|
573
|
+
errors.push({
|
|
574
|
+
path: `${path}.blocks.${index}.type`,
|
|
575
|
+
message: "Required",
|
|
576
|
+
expected: "string",
|
|
577
|
+
received: "undefined"
|
|
578
|
+
});
|
|
579
|
+
} else if (typeof block.type !== "string") {
|
|
580
|
+
errors.push(
|
|
581
|
+
createError(`${path}.blocks.${index}.type`, "string", block.type)
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return errors;
|
|
587
|
+
}
|
|
588
|
+
case "reference": {
|
|
589
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
590
|
+
return [createError(path, "object", value)];
|
|
591
|
+
}
|
|
592
|
+
const errors = [];
|
|
593
|
+
const requiredFields = ["id", "collection", "slug"];
|
|
594
|
+
for (const req of requiredFields) {
|
|
595
|
+
if (value[req] === void 0) {
|
|
596
|
+
errors.push({
|
|
597
|
+
path: `${path}.${req}`,
|
|
598
|
+
message: "Required",
|
|
599
|
+
expected: "string",
|
|
600
|
+
received: "undefined"
|
|
601
|
+
});
|
|
602
|
+
} else if (typeof value[req] !== "string") {
|
|
603
|
+
errors.push(createError(`${path}.${req}`, "string", value[req]));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return errors;
|
|
607
|
+
}
|
|
608
|
+
case "references": {
|
|
609
|
+
if (!Array.isArray(value)) {
|
|
610
|
+
return [createError(path, "array", value)];
|
|
611
|
+
}
|
|
612
|
+
return value.flatMap((item, index) => {
|
|
613
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
614
|
+
return [createError(`${path}.${index}`, "object", item)];
|
|
615
|
+
}
|
|
616
|
+
const errors = [];
|
|
617
|
+
const requiredFields = ["id", "collection", "slug"];
|
|
618
|
+
for (const req of requiredFields) {
|
|
619
|
+
if (item[req] === void 0) {
|
|
620
|
+
errors.push({
|
|
621
|
+
path: `${path}.${index}.${req}`,
|
|
622
|
+
message: "Required",
|
|
623
|
+
expected: "string",
|
|
624
|
+
received: "undefined"
|
|
625
|
+
});
|
|
626
|
+
} else if (typeof item[req] !== "string") {
|
|
627
|
+
errors.push(
|
|
628
|
+
createError(`${path}.${index}.${req}`, "string", item[req])
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return errors;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
default:
|
|
636
|
+
console.warn(`Unknown field type: ${field.type}`);
|
|
637
|
+
return [];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function getType(value) {
|
|
641
|
+
if (value === null) return "null";
|
|
642
|
+
if (Array.isArray(value)) return "array";
|
|
643
|
+
if (typeof value === "number" && Number.isNaN(value)) return "nan";
|
|
644
|
+
return typeof value;
|
|
645
|
+
}
|
|
646
|
+
function createError(path, expected, receivedValue) {
|
|
647
|
+
const received = getType(receivedValue);
|
|
648
|
+
return {
|
|
649
|
+
path,
|
|
650
|
+
message: `Expected ${expected}, received ${received}`,
|
|
651
|
+
expected,
|
|
652
|
+
received
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
365
656
|
// core/values.ts
|
|
366
657
|
function setValueAtPath(obj, path, value) {
|
|
367
658
|
const keys = path.split(".");
|
|
@@ -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
|
|
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 {
|
|
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
|
|
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.
|
|
1006
|
+
version: "3.0.1-beta.5",
|
|
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.
|
|
1175
|
+
"@blinkk/root": "3.0.1-beta.5",
|
|
949
1176
|
"firebase-admin": ">=11",
|
|
950
1177
|
"firebase-functions": ">=4"
|
|
951
1178
|
},
|