@caelo-cms/shared 0.10.24 → 0.10.26
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/ai-tools.d.ts +15 -10
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +13 -15
- package/dist/ai-tools.js.map +1 -1
- package/dist/build-page.d.ts +0 -2
- package/dist/build-page.d.ts.map +1 -1
- package/dist/build-page.js +2 -3
- package/dist/build-page.js.map +1 -1
- package/dist/content.d.ts +0 -3
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +91 -7
- package/dist/content.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/font-assets.d.ts +89 -0
- package/dist/font-assets.d.ts.map +1 -0
- package/dist/font-assets.js +61 -0
- package/dist/font-assets.js.map +1 -0
- package/dist/google-models.d.ts +35 -0
- package/dist/google-models.d.ts.map +1 -0
- package/dist/google-models.js +20 -0
- package/dist/google-models.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/media.d.ts +0 -3
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +0 -5
- package/dist/media.js.map +1 -1
- package/dist/plugin-preview.d.ts +44 -0
- package/dist/plugin-preview.d.ts.map +1 -0
- package/dist/plugin-preview.js +69 -0
- package/dist/plugin-preview.js.map +1 -0
- package/dist/preview-compose.d.ts +26 -0
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +92 -46
- package/dist/preview-compose.js.map +1 -1
- package/dist/responsive-images.d.ts +6 -5
- package/dist/responsive-images.d.ts.map +1 -1
- package/dist/responsive-images.js +6 -5
- package/dist/responsive-images.js.map +1 -1
- package/dist/seo.d.ts +10 -14
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +11 -12
- package/dist/seo.js.map +1 -1
- package/dist/template-engine.d.ts +18 -0
- package/dist/template-engine.d.ts.map +1 -1
- package/dist/template-engine.js +39 -2
- package/dist/template-engine.js.map +1 -1
- package/dist/theme-importers/loose.js +1 -1
- package/dist/theme-importers/loose.js.map +1 -1
- package/dist/theme-normalize.d.ts +9 -0
- package/dist/theme-normalize.d.ts.map +1 -1
- package/dist/theme-normalize.js +65 -6
- package/dist/theme-normalize.js.map +1 -1
- package/dist/theme-render.d.ts +15 -0
- package/dist/theme-render.d.ts.map +1 -1
- package/dist/theme-render.js +39 -0
- package/dist/theme-render.js.map +1 -1
- package/dist/themes.d.ts +15 -1
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +48 -1
- package/dist/themes.js.map +1 -1
- package/dist/url.d.ts +45 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +64 -0
- package/dist/url.js.map +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/redos-hardening.test.ts +1 -1
- package/src/ai-tools.ts +13 -15
- package/src/build-page.ts +3 -5
- package/src/content.test.ts +99 -17
- package/src/content.ts +95 -8
- package/src/context.ts +6 -0
- package/src/font-assets.ts +63 -0
- package/src/google-models.ts +20 -0
- package/src/index.ts +4 -2
- package/src/media.ts +0 -6
- package/src/plugin-preview.test.ts +79 -0
- package/src/plugin-preview.ts +81 -0
- package/src/preview-compose-deferrals.test.ts +144 -0
- package/src/preview-compose.test.ts +16 -0
- package/src/preview-compose.ts +125 -37
- package/src/responsive-images.ts +6 -5
- package/src/seo.test.ts +8 -8
- package/src/seo.ts +15 -23
- package/src/template-engine-data-lists.test.ts +90 -0
- package/src/template-engine.ts +72 -1
- package/src/theme-importers/loose.ts +1 -1
- package/src/theme-normalize.ts +70 -8
- package/src/theme-render.ts +36 -0
- package/src/theme-token-roles.test.ts +182 -0
- package/src/themes.ts +53 -0
- package/src/url.test.ts +40 -0
- package/src/url.ts +69 -0
- package/src/version.ts +1 -1
- package/dist/design-manifest.d.ts +0 -36
- package/dist/design-manifest.d.ts.map +0 -1
- package/dist/design-manifest.js +0 -90
- package/dist/design-manifest.js.map +0 -1
- package/dist/i18n.d.ts +0 -42
- package/dist/i18n.d.ts.map +0 -1
- package/dist/i18n.js +0 -85
- package/dist/i18n.js.map +0 -1
- package/src/design-manifest.ts +0 -93
- package/src/i18n.test.ts +0 -58
- package/src/i18n.ts +0 -91
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slug/URL primitives shared between the static generator, the preview
|
|
3
|
+
* op, and the admin UI (epic #380 #384 — successor of the deleted
|
|
4
|
+
* i18n.ts). URL shape beyond base + slug becomes a plugin contribution
|
|
5
|
+
* on the URL composition point (#390).
|
|
6
|
+
*
|
|
7
|
+
* isHomeSlug / isDesignatedHomePage — the home-decision predicate.
|
|
8
|
+
* trimSlashes / trimTrailingSlashes — linear-scan slug hygiene.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Strip leading and trailing `/` characters with a single linear scan.
|
|
12
|
+
*
|
|
13
|
+
* Replaces the regex `/^\/+|\/+$/g`, whose trailing-slash branch
|
|
14
|
+
* backtracks O(n²) on a long run of slashes that is not at end-of-string
|
|
15
|
+
* (CodeQL js/polynomial-redos). This char-index walk is unconditionally
|
|
16
|
+
* linear and produces byte-identical output for every input.
|
|
17
|
+
*/
|
|
18
|
+
export declare function trimSlashes(s: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Strip only trailing `/` characters with a single linear scan — the
|
|
21
|
+
* non-backtracking replacement for `/\/+$/` (same ReDoS class as above).
|
|
22
|
+
* Used for base URLs, which carry a scheme and must keep their leading
|
|
23
|
+
* characters.
|
|
24
|
+
*/
|
|
25
|
+
export declare function trimTrailingSlashes(s: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* The "magic slug" sentinel test: a page whose slug is empty, `home`,
|
|
28
|
+
* or `index` is the site root by convention even without an explicit
|
|
29
|
+
* `site_defaults.home_page_id` designation. Leading/trailing slashes
|
|
30
|
+
* are ignored so `/home/` reads the same as `home`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isHomeSlug(slug: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The single home-decision predicate every URL/output-path site must
|
|
35
|
+
* use so canonical, redirects, and the emitted file agree (0184
|
|
36
|
+
* explicit-homepage feature). A page is the site root when it IS the
|
|
37
|
+
* designated `site_defaults.home_page_id` OR carries a magic slug.
|
|
38
|
+
*
|
|
39
|
+
* @param pageId The page being resolved.
|
|
40
|
+
* @param slug That page's slug.
|
|
41
|
+
* @param designatedHomePageId `site_defaults.home_page_id`
|
|
42
|
+
* (null/undefined when none designated).
|
|
43
|
+
*/
|
|
44
|
+
export declare function isDesignatedHomePage(pageId: string, slug: string, designatedHomePageId: string | null | undefined): boolean;
|
|
45
|
+
//# sourceMappingURL=url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAM7C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAIrD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGhD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,oBAAoB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9C,OAAO,CAET"}
|
package/dist/url.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Slug/URL primitives shared between the static generator, the preview
|
|
4
|
+
* op, and the admin UI (epic #380 #384 — successor of the deleted
|
|
5
|
+
* i18n.ts). URL shape beyond base + slug becomes a plugin contribution
|
|
6
|
+
* on the URL composition point (#390).
|
|
7
|
+
*
|
|
8
|
+
* isHomeSlug / isDesignatedHomePage — the home-decision predicate.
|
|
9
|
+
* trimSlashes / trimTrailingSlashes — linear-scan slug hygiene.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Strip leading and trailing `/` characters with a single linear scan.
|
|
13
|
+
*
|
|
14
|
+
* Replaces the regex `/^\/+|\/+$/g`, whose trailing-slash branch
|
|
15
|
+
* backtracks O(n²) on a long run of slashes that is not at end-of-string
|
|
16
|
+
* (CodeQL js/polynomial-redos). This char-index walk is unconditionally
|
|
17
|
+
* linear and produces byte-identical output for every input.
|
|
18
|
+
*/
|
|
19
|
+
export function trimSlashes(s) {
|
|
20
|
+
let start = 0;
|
|
21
|
+
let end = s.length;
|
|
22
|
+
while (start < end && s.charCodeAt(start) === 47 /* '/' */)
|
|
23
|
+
start += 1;
|
|
24
|
+
while (end > start && s.charCodeAt(end - 1) === 47 /* '/' */)
|
|
25
|
+
end -= 1;
|
|
26
|
+
return s.slice(start, end);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Strip only trailing `/` characters with a single linear scan — the
|
|
30
|
+
* non-backtracking replacement for `/\/+$/` (same ReDoS class as above).
|
|
31
|
+
* Used for base URLs, which carry a scheme and must keep their leading
|
|
32
|
+
* characters.
|
|
33
|
+
*/
|
|
34
|
+
export function trimTrailingSlashes(s) {
|
|
35
|
+
let end = s.length;
|
|
36
|
+
while (end > 0 && s.charCodeAt(end - 1) === 47 /* '/' */)
|
|
37
|
+
end -= 1;
|
|
38
|
+
return s.slice(0, end);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The "magic slug" sentinel test: a page whose slug is empty, `home`,
|
|
42
|
+
* or `index` is the site root by convention even without an explicit
|
|
43
|
+
* `site_defaults.home_page_id` designation. Leading/trailing slashes
|
|
44
|
+
* are ignored so `/home/` reads the same as `home`.
|
|
45
|
+
*/
|
|
46
|
+
export function isHomeSlug(slug) {
|
|
47
|
+
const s = trimSlashes(slug);
|
|
48
|
+
return s === "" || s === "home" || s === "index";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The single home-decision predicate every URL/output-path site must
|
|
52
|
+
* use so canonical, redirects, and the emitted file agree (0184
|
|
53
|
+
* explicit-homepage feature). A page is the site root when it IS the
|
|
54
|
+
* designated `site_defaults.home_page_id` OR carries a magic slug.
|
|
55
|
+
*
|
|
56
|
+
* @param pageId The page being resolved.
|
|
57
|
+
* @param slug That page's slug.
|
|
58
|
+
* @param designatedHomePageId `site_defaults.home_page_id`
|
|
59
|
+
* (null/undefined when none designated).
|
|
60
|
+
*/
|
|
61
|
+
export function isDesignatedHomePage(pageId, slug, designatedHomePageId) {
|
|
62
|
+
return (designatedHomePageId != null && pageId === designatedHomePageId) || isHomeSlug(slug);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=url.js.map
|
package/dist/url.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.js","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC;;;;;;;;GAQG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,CAAS;IACnC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC;IACnB,OAAO,KAAK,GAAG,GAAG,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,SAAS;QAAE,KAAK,IAAI,CAAC,CAAC;IACvE,OAAO,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS;QAAE,GAAG,IAAI,CAAC,CAAC;IACvE,OAAO,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,CAAS;IAC3C,IAAI,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC;IACnB,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS;QAAE,GAAG,IAAI,CAAC,CAAC;IACnE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACzB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO,CAAC;AACnD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAc,EACd,IAAY,EACZ,oBAA+C;IAE/C,OAAO,CAAC,oBAAoB,IAAI,IAAI,IAAI,MAAM,KAAK,oBAAoB,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;AAC/F,CAAC"}
|
package/dist/version.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Format: SemVer 2.0.0. Pre-1.0 minor bumps are breaking; post-1.0
|
|
20
20
|
* follow standard SemVer.
|
|
21
21
|
*/
|
|
22
|
-
export declare const CAELO_VERSION = "0.10.
|
|
22
|
+
export declare const CAELO_VERSION = "0.10.26";
|
|
23
23
|
/**
|
|
24
24
|
* Deprecated alias for back-compat — early P17 work spelled this
|
|
25
25
|
* `CALEO_VERSION` (typo of "Caelo"). New code should import
|
|
@@ -31,7 +31,7 @@ export declare const CAELO_VERSION = "0.10.24";
|
|
|
31
31
|
*
|
|
32
32
|
* @deprecated use CAELO_VERSION
|
|
33
33
|
*/
|
|
34
|
-
export declare const CALEO_VERSION = "0.10.
|
|
34
|
+
export declare const CALEO_VERSION = "0.10.26";
|
|
35
35
|
/**
|
|
36
36
|
* Parsed shape — major/minor/patch + optional pre-release tag.
|
|
37
37
|
* Stable interface for callers that need to feature-gate (rare —
|
package/dist/version.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Format: SemVer 2.0.0. Pre-1.0 minor bumps are breaking; post-1.0
|
|
21
21
|
* follow standard SemVer.
|
|
22
22
|
*/
|
|
23
|
-
export const CAELO_VERSION = "0.10.
|
|
23
|
+
export const CAELO_VERSION = "0.10.26";
|
|
24
24
|
/**
|
|
25
25
|
* Deprecated alias for back-compat — early P17 work spelled this
|
|
26
26
|
* `CALEO_VERSION` (typo of "Caelo"). New code should import
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caelo-cms/shared",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.26",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"description": "Shared Zod schemas + types + the two-pass HTML composer used by every Caelo CMS workspace package. Stable enough for plugin authors and embedders to depend on directly.",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
import { describe, expect, it } from "bun:test";
|
|
14
14
|
import { fontFamilySlug, parseFontsCss } from "../fonts.js";
|
|
15
|
-
import { trimSlashes, trimTrailingSlashes } from "../i18n.js";
|
|
16
15
|
import { renderTemplate } from "../template-engine.js";
|
|
17
16
|
import { stripCssComments } from "../theme-importers/css-comments.js";
|
|
17
|
+
import { trimSlashes, trimTrailingSlashes } from "../url.js";
|
|
18
18
|
|
|
19
19
|
// 250ms, not 100: under CI coverage instrumentation on a loaded runner
|
|
20
20
|
// the LINEAR 100k-char scans measured up to ~113ms (PR #170 flake). A
|
package/src/ai-tools.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
setPlacementContentSchema,
|
|
25
25
|
slugSchema,
|
|
26
26
|
} from "./content.js";
|
|
27
|
+
import { pluginPreviewSelectionSchema } from "./plugin-preview.js";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* v0.6.2 — `position` argument shared across the three `add_module_to_*`
|
|
@@ -416,6 +417,7 @@ export const chatSendMessageInput = z
|
|
|
416
417
|
// operator content — it re-runs the paused gated turn. A refine below
|
|
417
418
|
// requires exactly one of content / resumeApproval.
|
|
418
419
|
content: z.string().min(1).max(8000).optional(),
|
|
420
|
+
previewSelection: pluginPreviewSelectionSchema.optional(),
|
|
419
421
|
/** Element-reference chips appended to the message. */
|
|
420
422
|
chips: z
|
|
421
423
|
.array(
|
|
@@ -505,6 +507,8 @@ export const chatPublishInput = z
|
|
|
505
507
|
// surface so chat-branched theme edits replay into live
|
|
506
508
|
// on chat.publish.
|
|
507
509
|
"theme",
|
|
510
|
+
// A plugin private-storage row written on the branch.
|
|
511
|
+
"pluginRow",
|
|
508
512
|
]),
|
|
509
513
|
entityId: z.string().uuid(),
|
|
510
514
|
})
|
|
@@ -683,7 +687,6 @@ export const duplicatePageToolInput = z
|
|
|
683
687
|
newName: z.string().min(1).max(256).optional(),
|
|
684
688
|
newTitle: z.string().min(1).max(256).optional(),
|
|
685
689
|
targetTemplateId: z.string().uuid().optional(),
|
|
686
|
-
locale: z.string().min(2).max(10).optional(),
|
|
687
690
|
})
|
|
688
691
|
.strict();
|
|
689
692
|
|
|
@@ -731,11 +734,11 @@ export const reorderModuleToolInput = z
|
|
|
731
734
|
|
|
732
735
|
/**
|
|
733
736
|
* P7 — `find_media`. Searches the media library by alt-text /
|
|
734
|
-
* filename / mime.
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
737
|
+
* filename / mime. Each row carries the media UUID (what
|
|
738
|
+
* `set_theme_asset` / `set_media_alt` require) plus a pre-resolved
|
|
739
|
+
* variant URL. The AI's only media-inventory surface — the `## Media`
|
|
740
|
+
* system-prompt block was removed by the context diet (#300), so every
|
|
741
|
+
* asset lookup ("an image of a sunlit office") goes through this tool.
|
|
739
742
|
*/
|
|
740
743
|
export const findMediaToolInput = z
|
|
741
744
|
.object({
|
|
@@ -790,16 +793,11 @@ export const setMediaAltToolInput = z
|
|
|
790
793
|
export type SetMediaAltToolInput = z.infer<typeof setMediaAltToolInput>;
|
|
791
794
|
|
|
792
795
|
/**
|
|
793
|
-
* 0184 — `set_home_page`. AI designates a page as the site homepage
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
+
* 0184 — `set_home_page`. AI designates a page as the site homepage.
|
|
797
|
+
* Records `site_defaults.home_page_id`; the page keeps its own slug but
|
|
798
|
+
* resolves to `/`.
|
|
796
799
|
*/
|
|
797
|
-
export const setHomePageToolInput = z
|
|
798
|
-
.object({
|
|
799
|
-
pageId: z.string().uuid(),
|
|
800
|
-
locale: z.string().min(2).max(10).optional(),
|
|
801
|
-
})
|
|
802
|
-
.strict();
|
|
800
|
+
export const setHomePageToolInput = z.object({ pageId: z.string().uuid() }).strict();
|
|
803
801
|
export type SetHomePageToolInput = z.infer<typeof setHomePageToolInput>;
|
|
804
802
|
|
|
805
803
|
/**
|
package/src/build-page.ts
CHANGED
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
import { z } from "zod";
|
|
23
23
|
import {
|
|
24
24
|
contentInstanceCreateSchema,
|
|
25
|
-
localeSchema,
|
|
26
25
|
MODULE_CSS_MAX,
|
|
27
26
|
MODULE_HTML_MAX,
|
|
28
27
|
MODULE_JS_MAX,
|
|
@@ -194,7 +193,6 @@ export const buildPageTargetSchema = z
|
|
|
194
193
|
slug: slugSchema.optional(),
|
|
195
194
|
title: z.string().min(1).max(256).optional(),
|
|
196
195
|
name: z.string().min(1).max(256).optional(),
|
|
197
|
-
locale: localeSchema.optional(),
|
|
198
196
|
templateId: z.string().uuid().optional(),
|
|
199
197
|
status: pageStatusSchema.optional(),
|
|
200
198
|
/**
|
|
@@ -210,9 +208,9 @@ export const buildPageTargetSchema = z
|
|
|
210
208
|
})
|
|
211
209
|
.strict()
|
|
212
210
|
.superRefine((page, ctx) => {
|
|
213
|
-
const createKeys = (
|
|
214
|
-
|
|
215
|
-
)
|
|
211
|
+
const createKeys = (["slug", "title", "name", "templateId", "status"] as const).filter(
|
|
212
|
+
(k) => page[k] !== undefined,
|
|
213
|
+
);
|
|
216
214
|
if (page.pageId !== undefined) {
|
|
217
215
|
if (createKeys.length > 0) {
|
|
218
216
|
ctx.addIssue({
|
package/src/content.test.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "bun:test";
|
|
4
4
|
import {
|
|
5
|
-
localeSchema,
|
|
6
5
|
MODULE_HTML_MAX,
|
|
7
6
|
moduleCreateSchema,
|
|
7
|
+
moduleUpdateSchema,
|
|
8
8
|
pageCreateSchema,
|
|
9
9
|
pageSetModulesSchema,
|
|
10
10
|
pageUpdateSchema,
|
|
@@ -29,21 +29,6 @@ describe("slugSchema", () => {
|
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
describe("localeSchema", () => {
|
|
33
|
-
it("accepts language and language-region", () => {
|
|
34
|
-
expect(localeSchema.safeParse("en").success).toBe(true);
|
|
35
|
-
expect(localeSchema.safeParse("de").success).toBe(true);
|
|
36
|
-
expect(localeSchema.safeParse("de-AT").success).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("rejects malformed locales", () => {
|
|
40
|
-
expect(localeSchema.safeParse("EN").success).toBe(false);
|
|
41
|
-
expect(localeSchema.safeParse("en-us").success).toBe(false);
|
|
42
|
-
expect(localeSchema.safeParse("eng").success).toBe(false);
|
|
43
|
-
expect(localeSchema.safeParse("").success).toBe(false);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
32
|
describe("moduleCreateSchema", () => {
|
|
48
33
|
it("accepts a minimal module", () => {
|
|
49
34
|
const r = moduleCreateSchema.safeParse({
|
|
@@ -73,6 +58,104 @@ describe("moduleCreateSchema", () => {
|
|
|
73
58
|
});
|
|
74
59
|
expect(r.success).toBe(false);
|
|
75
60
|
});
|
|
61
|
+
|
|
62
|
+
// issue #432 — CDATA wrappers stored verbatim broke rendering and sent the
|
|
63
|
+
// AI into a ~20-loop visual repair spiral; the write boundary now rejects
|
|
64
|
+
// them with an actionable message.
|
|
65
|
+
it("rejects XML CDATA wrappers in css and js, with an actionable message", () => {
|
|
66
|
+
const base = { slug: "site-header", displayName: "Site header", html: "<header></header>" };
|
|
67
|
+
const css = moduleCreateSchema.safeParse({
|
|
68
|
+
...base,
|
|
69
|
+
css: "/*<![CDATA[*/ .x { color: red } /*]]>*/",
|
|
70
|
+
});
|
|
71
|
+
expect(css.success).toBe(false);
|
|
72
|
+
if (!css.success) {
|
|
73
|
+
expect(css.error.issues.map((i) => i.message).join(" ")).toContain("CDATA");
|
|
74
|
+
}
|
|
75
|
+
const js = moduleCreateSchema.safeParse({
|
|
76
|
+
...base,
|
|
77
|
+
js: "//<![CDATA[\nconsole.log('hi');\n//]]>",
|
|
78
|
+
});
|
|
79
|
+
expect(js.success).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("accepts plain css/js (and js merely mentioning ']]>' in a string)", () => {
|
|
83
|
+
const base = { slug: "site-header", displayName: "Site header", html: "<header></header>" };
|
|
84
|
+
expect(
|
|
85
|
+
moduleCreateSchema.safeParse({ ...base, css: ".x { color: red }", js: "console.log(1);" })
|
|
86
|
+
.success,
|
|
87
|
+
).toBe(true);
|
|
88
|
+
// Only the OPENING marker is the corruption signature; a bare "]]>"
|
|
89
|
+
// inside a string literal is legitimate (e.g. code emitting XML).
|
|
90
|
+
expect(moduleCreateSchema.safeParse({ ...base, js: 'xml += "]]>";' }).success).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rejects CDATA on the update path too (edit_module / edit_content funnel)", () => {
|
|
94
|
+
const r = moduleUpdateSchema.safeParse({
|
|
95
|
+
moduleId: "8c2f2f2a-0000-4000-8000-000000000432",
|
|
96
|
+
css: "/*<![CDATA[*/ .x{} /*]]>*/",
|
|
97
|
+
});
|
|
98
|
+
expect(r.success).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// CLAUDE.md §1A / issue #417 — repeating content is a list field, never
|
|
103
|
+
// numbered scalars. Threshold is 3: a pair (col1/col2) is a legitimate
|
|
104
|
+
// layout split; three-or-more numbered siblings is a list.
|
|
105
|
+
describe("moduleFields numbered-scalar fanout guard (§1A)", () => {
|
|
106
|
+
const mod = (fields: unknown) =>
|
|
107
|
+
moduleCreateSchema.safeParse({
|
|
108
|
+
slug: "cards",
|
|
109
|
+
displayName: "Cards",
|
|
110
|
+
html: "<div></div>",
|
|
111
|
+
fields,
|
|
112
|
+
});
|
|
113
|
+
const text = (name: string) => ({ name, kind: "text", label: name });
|
|
114
|
+
|
|
115
|
+
it("rejects a bare base + numbered siblings (label, label2, label3)", () => {
|
|
116
|
+
const r = mod([text("label"), text("label2"), text("label3")]);
|
|
117
|
+
expect(r.success).toBe(false);
|
|
118
|
+
if (!r.success) {
|
|
119
|
+
const msg = r.error.issues[0]?.message ?? "";
|
|
120
|
+
expect(msg).toContain("numbered-scalar fanout");
|
|
121
|
+
expect(msg).toContain('"label", "label2", "label3"');
|
|
122
|
+
// AI-actionable: the error names every list-kind fix.
|
|
123
|
+
expect(msg).toContain("text-list");
|
|
124
|
+
expect(msg).toContain("link-list");
|
|
125
|
+
expect(msg).toContain("module-list");
|
|
126
|
+
expect(msg).toContain("§1A");
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("rejects underscore-separated runs (quote_1..quote_3) and mixed separators", () => {
|
|
131
|
+
expect(mod([text("quote_1"), text("quote_2"), text("quote_3")]).success).toBe(false);
|
|
132
|
+
expect(mod([text("logo1"), text("logo_2"), text("logo_3")]).success).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("rejects fanout on moduleUpdateSchema too (same shared fields array)", () => {
|
|
136
|
+
const r = moduleUpdateSchema.safeParse({
|
|
137
|
+
moduleId: "6f9619ff-8b86-4d01-b42d-00cf4fc964ff",
|
|
138
|
+
fields: [text("card1"), text("card2"), text("card3")],
|
|
139
|
+
});
|
|
140
|
+
expect(r.success).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("allows a two-field split (col1/col2) — threshold is 3", () => {
|
|
144
|
+
expect(mod([text("col1"), text("col2")]).success).toBe(true);
|
|
145
|
+
expect(mod([text("label"), text("label2")]).success).toBe(true);
|
|
146
|
+
expect(mod([text("address_line1"), text("address_line2")]).success).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("allows unrelated numbered fields that never form a run of 3", () => {
|
|
150
|
+
expect(mod([text("hero_title"), text("cta_2024"), text("step_1")]).success).toBe(true);
|
|
151
|
+
// Same suffix, different bases — no shared run.
|
|
152
|
+
expect(mod([text("left_col1"), text("right_col1"), text("mid_col1")]).success).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("scopes to scalar kinds — numbered list fields are a modelling question, not fanout", () => {
|
|
156
|
+
const linkList = (name: string) => ({ name, kind: "link-list", label: name });
|
|
157
|
+
expect(mod([linkList("links1"), linkList("links2"), linkList("links3")]).success).toBe(true);
|
|
158
|
+
});
|
|
76
159
|
});
|
|
77
160
|
|
|
78
161
|
describe("pageCreateSchema (no raw HTML invariant)", () => {
|
|
@@ -84,7 +167,6 @@ describe("pageCreateSchema (no raw HTML invariant)", () => {
|
|
|
84
167
|
});
|
|
85
168
|
expect(r.success).toBe(true);
|
|
86
169
|
if (!r.success) return;
|
|
87
|
-
expect(r.data.locale).toBe("en");
|
|
88
170
|
// status is intentionally OPTIONAL (no schema default): pages.create
|
|
89
171
|
// resolves it at write time — published on a bootstrap site, else draft.
|
|
90
172
|
// Omitting it here leaves it undefined for the handler to resolve.
|
package/src/content.ts
CHANGED
|
@@ -84,11 +84,6 @@ export function slugifyModuleSection(
|
|
|
84
84
|
return `${deriveModuleType(displayName)}-${idx}-${suffix}`;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/** BCP-47 shape: 2-letter language with optional 2-letter region. P9 widens this. */
|
|
88
|
-
export const localeSchema = z
|
|
89
|
-
.string()
|
|
90
|
-
.regex(/^[a-z]{2}(-[A-Z]{2})?$/, "locale must be 'xx' or 'xx-YY' (BCP-47 shape)");
|
|
91
|
-
|
|
92
87
|
const displayNameSchema = z.string().min(1).max(128);
|
|
93
88
|
|
|
94
89
|
/** Caps are arbitrary but cheap — paste-of-binary mistakes get caught. */
|
|
@@ -99,8 +94,39 @@ export const TEMPLATE_HTML_MAX = 512 * 1024;
|
|
|
99
94
|
export const TEMPLATE_CSS_MAX = 128 * 1024;
|
|
100
95
|
|
|
101
96
|
const moduleHtml = z.string().max(MODULE_HTML_MAX, `html exceeds ${MODULE_HTML_MAX} bytes`);
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* issue #432 root-cause guard — reject XML CDATA wrappers in module css/js.
|
|
100
|
+
*
|
|
101
|
+
* Module css/js is stored and served VERBATIM, so a `<![CDATA[ … ]]>`
|
|
102
|
+
* wrapper (an XHTML-era idiom the AI occasionally reaches for when
|
|
103
|
+
* "protecting" inline code) reaches the browser unparsed: the stylesheet
|
|
104
|
+
* fails to apply and the script throws `SyntaxError: Unexpected token '<'`.
|
|
105
|
+
* On e2e runs 31000584991 / 31000699422 (2026-08-05) exactly this landed
|
|
106
|
+
* silently, the page rendered broken, and the model burned ~15–20 extra
|
|
107
|
+
* loops (~+1.2M input tokens) discovering and repairing it via
|
|
108
|
+
* screenshot/inspect/edit cycles. Rejecting at the write boundary with an
|
|
109
|
+
* actionable message turns that repair spiral into a single corrected call
|
|
110
|
+
* (CLAUDE.md §11 — failure surfaces are AI-actionable; §2 — fail loudly).
|
|
111
|
+
*
|
|
112
|
+
* css/js only: CDATA is never valid CSS or JS, while an html body may
|
|
113
|
+
* legitimately carry CDATA inside inline `<svg>`/`<math>` foreign content.
|
|
114
|
+
*/
|
|
115
|
+
const CDATA_OPEN = "<![CDATA[";
|
|
116
|
+
const noCdata = (value: string): boolean => !value.includes(CDATA_OPEN);
|
|
117
|
+
const cdataMessage = (field: "css" | "js"): string =>
|
|
118
|
+
`${field} contains an XML CDATA marker (${CDATA_OPEN}) — module ${field} is stored and served ` +
|
|
119
|
+
`verbatim, so CDATA wrappers reach the browser unparsed and break rendering. ` +
|
|
120
|
+
`Remove the wrapper and resend plain ${field === "css" ? "CSS" : "JavaScript"}.`;
|
|
121
|
+
|
|
122
|
+
const moduleCss = z
|
|
123
|
+
.string()
|
|
124
|
+
.max(MODULE_CSS_MAX, `css exceeds ${MODULE_CSS_MAX} bytes`)
|
|
125
|
+
.refine(noCdata, cdataMessage("css"));
|
|
126
|
+
const moduleJs = z
|
|
127
|
+
.string()
|
|
128
|
+
.max(MODULE_JS_MAX, `js exceeds ${MODULE_JS_MAX} bytes`)
|
|
129
|
+
.refine(noCdata, cdataMessage("js"));
|
|
104
130
|
|
|
105
131
|
/**
|
|
106
132
|
* v0.4.0 — module field schema. Each field declares one substitution slot in
|
|
@@ -283,6 +309,54 @@ export const moduleRefSchema = z
|
|
|
283
309
|
.strict();
|
|
284
310
|
export type ModuleRef = z.infer<typeof moduleRefSchema>;
|
|
285
311
|
|
|
312
|
+
/**
|
|
313
|
+
* CLAUDE.md §1A — "repeating content is a list field, never numbered
|
|
314
|
+
* scalars". This guard rejects the mechanically detectable half of that
|
|
315
|
+
* rule: a RUN of 3+ sibling scalar fields sharing a base name plus a
|
|
316
|
+
* numeric suffix (`label`, `label2`, `label3` / `quote_1`…`quote_3`).
|
|
317
|
+
*
|
|
318
|
+
* Threshold is 3 (issue #417): a two-field split (`col1`/`col2` for a
|
|
319
|
+
* two-column layout) is routinely a genuine layout role, so a pair stays
|
|
320
|
+
* legal; three-or-more numbered siblings is repeating content that must
|
|
321
|
+
* be ONE list field. The bare base name counts as an implicit member of
|
|
322
|
+
* its run — `label` next to `label2`/`label3` is item 1 in disguise.
|
|
323
|
+
*
|
|
324
|
+
* Deliberately out of scope (guidance-only, per the issue): content-
|
|
325
|
+
* derived names (`quote_justamazing`) and mid-name numbering
|
|
326
|
+
* (`step1_title`) — neither is detectable without false positives.
|
|
327
|
+
* Nested/list kinds are excluded too: a numbered run of lists is
|
|
328
|
+
* already a modelling question, not scalar fanout. The server-side
|
|
329
|
+
* extractor fallback (`extract-module-structure.ts`) is also
|
|
330
|
+
* unguarded on purpose: it numbers repeated tags in messy HUMAN-
|
|
331
|
+
* authored HTML, runs only when the caller declared no fields, and its
|
|
332
|
+
* output never re-enters this schema — the guard polices the boundary
|
|
333
|
+
* where the AI (or a human) AUTHORS `fields[]` explicitly.
|
|
334
|
+
*/
|
|
335
|
+
const NUMBERED_SCALAR_FANOUT_THRESHOLD = 3;
|
|
336
|
+
const numberedFieldName = /^([a-z][a-z0-9_]*?)_?\d+$/;
|
|
337
|
+
|
|
338
|
+
function findNumberedScalarFanoutRuns(
|
|
339
|
+
fields: readonly { name: string; kind: string }[],
|
|
340
|
+
): string[][] {
|
|
341
|
+
const scalarKinds = new Set<string>(MODULE_FIELD_PRIMITIVE_KINDS);
|
|
342
|
+
const scalarNames = fields.filter((f) => scalarKinds.has(f.kind)).map((f) => f.name);
|
|
343
|
+
const byBase = new Map<string, string[]>();
|
|
344
|
+
for (const name of scalarNames) {
|
|
345
|
+
const m = numberedFieldName.exec(name);
|
|
346
|
+
const base = m?.[1];
|
|
347
|
+
if (base === undefined) continue;
|
|
348
|
+
const members = byBase.get(base);
|
|
349
|
+
if (members) members.push(name);
|
|
350
|
+
else byBase.set(base, [name]);
|
|
351
|
+
}
|
|
352
|
+
const runs: string[][] = [];
|
|
353
|
+
for (const [base, members] of byBase) {
|
|
354
|
+
const run = scalarNames.includes(base) ? [base, ...members] : members;
|
|
355
|
+
if (run.length >= NUMBERED_SCALAR_FANOUT_THRESHOLD) runs.push(run);
|
|
356
|
+
}
|
|
357
|
+
return runs;
|
|
358
|
+
}
|
|
359
|
+
|
|
286
360
|
const moduleFieldsArray = z
|
|
287
361
|
.array(moduleFieldSchema)
|
|
288
362
|
.max(64, "modules may declare at most 64 fields")
|
|
@@ -295,6 +369,20 @@ const moduleFieldsArray = z
|
|
|
295
369
|
}
|
|
296
370
|
seen.add(f.name);
|
|
297
371
|
}
|
|
372
|
+
for (const run of findNumberedScalarFanoutRuns(arr)) {
|
|
373
|
+
ctx.addIssue({
|
|
374
|
+
code: z.ZodIssueCode.custom,
|
|
375
|
+
message:
|
|
376
|
+
`numbered-scalar fanout: ${run.map((n) => `"${n}"`).join(", ")} repeat one role as ` +
|
|
377
|
+
"numbered scalar fields — repeating content is ONE list field, never numbered scalars " +
|
|
378
|
+
'(CLAUDE.md §1A). Replace the run with a single field: kind "text-list" (list of ' +
|
|
379
|
+
'strings, inner template iterates {{.}}), "link-list" (list of {label, href}), or ' +
|
|
380
|
+
'"module-list" (rich per-item structure: mint a card sub-module and reference it per ' +
|
|
381
|
+
"item — in build_page, author the card detached with `ref` and pass " +
|
|
382
|
+
'[{"$ref": "<ref>"}, …] as the field value). A two-field split like col1/col2 is fine; ' +
|
|
383
|
+
`${NUMBERED_SCALAR_FANOUT_THRESHOLD}+ numbered siblings is a list.`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
298
386
|
});
|
|
299
387
|
|
|
300
388
|
/**
|
|
@@ -457,7 +545,6 @@ export const pageStatusSchema = z.enum(["draft", "published"]);
|
|
|
457
545
|
export const pageCreateSchema = z
|
|
458
546
|
.object({
|
|
459
547
|
slug: slugSchema,
|
|
460
|
-
locale: localeSchema.default("en"),
|
|
461
548
|
/** P6.7.5 — internal editor label. Defaults to title if omitted. */
|
|
462
549
|
name: z.string().min(1).max(256).optional(),
|
|
463
550
|
title: z.string().min(1).max(256),
|
package/src/context.ts
CHANGED
|
@@ -31,6 +31,12 @@ export interface ExecutionContext {
|
|
|
31
31
|
* fold consecutive AI actions into one entry.
|
|
32
32
|
*/
|
|
33
33
|
readonly chatTaskId?: string;
|
|
34
|
+
/**
|
|
35
|
+
* For a plugin installed as an external artifact: the digest of the
|
|
36
|
+
* artifact this call runs. Private storage operations accept the call
|
|
37
|
+
* only while exactly this artifact is the active, granted one.
|
|
38
|
+
*/
|
|
39
|
+
readonly pluginArtifactDigest?: string;
|
|
34
40
|
/**
|
|
35
41
|
* P10.5: parent attribution for subagent invocations. When the
|
|
36
42
|
* `spawn_subagent` tool calls runChatTurn for the child, it carries
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const fontRef = z
|
|
5
|
+
.object({ id: z.string().uuid(), sha256: z.string().regex(/^[a-f0-9]{64}$/) })
|
|
6
|
+
.strict();
|
|
7
|
+
export const fontLicense = z
|
|
8
|
+
.object({
|
|
9
|
+
name: z.string().min(1).max(200),
|
|
10
|
+
text: z.string().min(1).max(100_000),
|
|
11
|
+
webEmbedding: z.boolean(),
|
|
12
|
+
documentEmbedding: z.boolean(),
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
export const fontMetadata = fontRef.extend({
|
|
16
|
+
family: z.string(),
|
|
17
|
+
subfamily: z.string(),
|
|
18
|
+
postscriptName: z.string(),
|
|
19
|
+
cssFamily: z.string(),
|
|
20
|
+
format: z.enum(["ttf", "otf", "woff", "woff2"]),
|
|
21
|
+
weight: z.number(),
|
|
22
|
+
style: z.enum(["normal", "italic"]),
|
|
23
|
+
axes: z.record(z.string(), z.object({ min: z.number(), max: z.number(), default: z.number() })),
|
|
24
|
+
sizeBytes: z.number(),
|
|
25
|
+
glyphCount: z.number(),
|
|
26
|
+
embedding: z.object({
|
|
27
|
+
web: z.boolean(),
|
|
28
|
+
document: z.boolean(),
|
|
29
|
+
subset: z.boolean(),
|
|
30
|
+
restriction: z.string().nullable(),
|
|
31
|
+
}),
|
|
32
|
+
license: fontLicense,
|
|
33
|
+
source: z.string(),
|
|
34
|
+
createdAt: z.string(),
|
|
35
|
+
});
|
|
36
|
+
export type FontRef = z.infer<typeof fontRef>;
|
|
37
|
+
export type FontMetadata = z.infer<typeof fontMetadata>;
|
|
38
|
+
export const fontImportInput = z
|
|
39
|
+
.object({
|
|
40
|
+
dataBase64: z.string().min(1).max(11_184_812),
|
|
41
|
+
license: fontLicense,
|
|
42
|
+
source: z.string().min(1).max(1000),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
export const fontResolveInput = fontRef
|
|
46
|
+
.extend({
|
|
47
|
+
use: z.enum(["web", "document"]),
|
|
48
|
+
text: z.string().max(200_000).default(""),
|
|
49
|
+
formats: z
|
|
50
|
+
.array(z.enum(["ttf", "otf", "woff", "woff2"]))
|
|
51
|
+
.min(1)
|
|
52
|
+
.max(4),
|
|
53
|
+
})
|
|
54
|
+
.strict();
|
|
55
|
+
export const fontReadInput = fontRef
|
|
56
|
+
.extend({ offset: z.number().int().min(0), length: z.number().int().min(1).max(262144) })
|
|
57
|
+
.strict();
|
|
58
|
+
export const fontFindInput = z
|
|
59
|
+
.object({
|
|
60
|
+
query: z.string().max(200).default(""),
|
|
61
|
+
limit: z.number().int().min(1).max(100).default(50),
|
|
62
|
+
})
|
|
63
|
+
.strict();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/** Curated against https://ai.google.dev/gemini-api/docs/models on 2026-09-11.
|
|
4
|
+
* Keep image-only models out of the chat picker: they cannot call tools.
|
|
5
|
+
* Exact IDs are intentional; automatic latest aliases could change behaviour.
|
|
6
|
+
*/
|
|
7
|
+
export const GOOGLE_CHAT_MODELS = [
|
|
8
|
+
{ id: "gemini-3.8-flash", label: "Gemini 3.8 Flash" },
|
|
9
|
+
{ id: "gemini-3.5-flash-lite", label: "Gemini 3.5 Flash-Lite" },
|
|
10
|
+
{ id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (Preview)" },
|
|
11
|
+
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
|
12
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
|
|
13
|
+
] as const;
|
|
14
|
+
export const DEFAULT_GOOGLE_CHAT_MODEL = "gemini-3.8-flash";
|
|
15
|
+
export const GOOGLE_IMAGE_MODELS = [
|
|
16
|
+
{ id: "gemini-3.1-flash-image", label: "Nano Banana 2 (Gemini 3.1 Flash Image)" },
|
|
17
|
+
{ id: "gemini-3.1-flash-lite-image", label: "Nano Banana 2 Lite" },
|
|
18
|
+
{ id: "gemini-3-pro-image", label: "Nano Banana Pro (Gemini 3 Pro Image)" },
|
|
19
|
+
{ id: "gemini-2.5-flash-image", label: "Nano Banana (Gemini 2.5 Flash Image)" },
|
|
20
|
+
] as const;
|