@openstaticfish/chattybox-config 0.2.0 → 0.3.0
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/README.md +17 -4
- package/dist/index.d.ts +21 -0
- package/dist/index.js +141 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,12 +7,25 @@ import { defineConfig } from "@openstaticfish/chattybox-config";
|
|
|
7
7
|
|
|
8
8
|
export default defineConfig({
|
|
9
9
|
schemaVersion: "1",
|
|
10
|
-
|
|
10
|
+
project: { description: "Managed from Git" },
|
|
11
|
+
assistant: { name: "Support", promptProfile: "support" },
|
|
11
12
|
knowledge: {
|
|
12
|
-
sources: [{
|
|
13
|
+
sources: [{
|
|
14
|
+
type: "website",
|
|
15
|
+
url: "https://docs.example.com",
|
|
16
|
+
mode: "sitemap",
|
|
17
|
+
sitemapUrl: "https://docs.example.com/sitemap.xml",
|
|
18
|
+
autoRescrape: true,
|
|
19
|
+
rescrapeInterval: "daily",
|
|
20
|
+
}],
|
|
13
21
|
},
|
|
14
|
-
|
|
22
|
+
runtime: { localeMode: "auto", defaultLocale: "en", allowLocaleOverride: true },
|
|
23
|
+
widget: { enabled: true, icon: { type: "emoji", emoji: "💬" } },
|
|
15
24
|
});
|
|
16
25
|
```
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
Schema version 1 supports exactly one website source per project when `knowledge` is provided. Preview and production deployments require that source for an in-place, non-destructive refresh. Discovery modes are `homepage`, `sitemap`, `manual`, and `crawl`; omitting mode means crawl and preserves an explicit `maxDepth`, including zero. Source changes and include/exclude filters govern discovery, not deletion of existing indexed pages. Existing content consumes page capacity, so check refresh outcomes when changing sources.
|
|
28
|
+
|
|
29
|
+
Unknown fields and empty include/exclude entries are rejected. Preview and production are promotion labels for one shared project runtime, not isolated environments. Use separate projects for independent runtimes.
|
|
30
|
+
|
|
31
|
+
This contract requires config and CLI 0.3.0 or later. Version 0.2.0 lacks several fields in this example; use compatible config and CLI versions together.
|
package/dist/index.d.ts
CHANGED
|
@@ -3,12 +3,21 @@ export type DeploymentEnvironment = "development" | "preview" | "production";
|
|
|
3
3
|
export type WebsiteSource = {
|
|
4
4
|
type: "website";
|
|
5
5
|
url: string;
|
|
6
|
+
mode?: "homepage" | "sitemap" | "manual" | "crawl";
|
|
7
|
+
sitemapUrl?: string;
|
|
8
|
+
manualUrls?: Array<string>;
|
|
6
9
|
include?: Array<string>;
|
|
7
10
|
exclude?: Array<string>;
|
|
8
11
|
maxPages?: number;
|
|
12
|
+
maxDepth?: number;
|
|
13
|
+
autoRescrape?: boolean;
|
|
14
|
+
rescrapeInterval?: "daily" | "weekly" | "monthly";
|
|
9
15
|
};
|
|
10
16
|
export type ChattyboxConfig = {
|
|
11
17
|
schemaVersion: "1";
|
|
18
|
+
project?: {
|
|
19
|
+
description?: string;
|
|
20
|
+
};
|
|
12
21
|
assistant: {
|
|
13
22
|
name: string;
|
|
14
23
|
systemPrompt?: string;
|
|
@@ -19,6 +28,10 @@ export type ChattyboxConfig = {
|
|
|
19
28
|
sources: Array<WebsiteSource>;
|
|
20
29
|
};
|
|
21
30
|
runtime?: {
|
|
31
|
+
localeMode?: "auto" | "fixed";
|
|
32
|
+
defaultLocale?: string;
|
|
33
|
+
allowLocaleOverride?: boolean;
|
|
34
|
+
/** Shorthand for fixed mode with locale overrides disabled. */
|
|
22
35
|
locale?: string;
|
|
23
36
|
};
|
|
24
37
|
widget?: {
|
|
@@ -26,6 +39,14 @@ export type ChattyboxConfig = {
|
|
|
26
39
|
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
27
40
|
headerTitle?: string;
|
|
28
41
|
welcomeMessage?: string;
|
|
42
|
+
/** Starter and higher plans may hide the maintained widget attribution. */
|
|
43
|
+
showBranding?: boolean;
|
|
44
|
+
icon?: {
|
|
45
|
+
type: "default" | "emoji" | "url";
|
|
46
|
+
size?: "small" | "medium" | "large";
|
|
47
|
+
emoji?: string;
|
|
48
|
+
url?: string;
|
|
49
|
+
};
|
|
29
50
|
theme?: {
|
|
30
51
|
primaryColor?: string;
|
|
31
52
|
backgroundColor?: string;
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,23 @@ var HEX_COLOR = /^#[0-9a-f]{6}$/i;
|
|
|
3
3
|
var PROMPT_PROFILES = new Set(["default", "support", "sales", "sarcastic", "custom"]);
|
|
4
4
|
var WIDGET_POSITIONS = new Set(["bottom-right", "bottom-left", "top-right", "top-left"]);
|
|
5
5
|
var WIDGET_LOCALES = new Set(["en", "de", "fr", "es", "it", "nl", "pl", "pt", "sv", "fi", "et", "cs", "cy", "id"]);
|
|
6
|
+
var SOURCE_MODES = new Set(["homepage", "sitemap", "manual", "crawl"]);
|
|
7
|
+
var RESCRAPE_INTERVALS = new Set(["daily", "weekly", "monthly"]);
|
|
8
|
+
var ICON_TYPES = new Set(["default", "emoji", "url"]);
|
|
9
|
+
var ICON_SIZES = new Set(["small", "medium", "large"]);
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function requireRecord(value, path, keys) {
|
|
14
|
+
if (!isRecord(value))
|
|
15
|
+
throw new Error(`${path} must be an object`);
|
|
16
|
+
if (keys) {
|
|
17
|
+
for (const key of Object.keys(value)) {
|
|
18
|
+
if (!keys.includes(key))
|
|
19
|
+
throw new Error(`${path}.${key} is not supported`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
6
23
|
function requireNonEmptyString(value, path) {
|
|
7
24
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
8
25
|
throw new Error(`${path} must be a non-empty string`);
|
|
@@ -13,50 +30,155 @@ function validateOptionalColor(value, path) {
|
|
|
13
30
|
throw new Error(`${path} must be a six-digit hex color`);
|
|
14
31
|
}
|
|
15
32
|
}
|
|
33
|
+
function validateOptionalStringArray(value, path) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return;
|
|
36
|
+
if (!Array.isArray(value))
|
|
37
|
+
throw new Error(`${path} must be an array of strings`);
|
|
38
|
+
value.forEach((entry, index) => requireNonEmptyString(entry, `${path}[${index}]`));
|
|
39
|
+
}
|
|
40
|
+
function validateHttpUrl(value, path) {
|
|
41
|
+
requireNonEmptyString(value, path);
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(value);
|
|
44
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
45
|
+
throw new Error;
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error(`${path} must be an HTTP(S) URL`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
16
50
|
function validateConfig(config) {
|
|
17
|
-
if (!config
|
|
51
|
+
if (!isRecord(config))
|
|
18
52
|
throw new Error("Config must be an object");
|
|
53
|
+
requireRecord(config, "Config", ["schemaVersion", "project", "assistant", "knowledge", "runtime", "widget"]);
|
|
19
54
|
if (config.schemaVersion !== "1")
|
|
20
55
|
throw new Error('schemaVersion must be "1"');
|
|
21
|
-
|
|
56
|
+
requireRecord(config.assistant, "assistant", ["name", "systemPrompt", "promptProfile", "fallbackMessage"]);
|
|
57
|
+
requireNonEmptyString(config.assistant.name, "assistant.name");
|
|
58
|
+
if (config.project !== undefined) {
|
|
59
|
+
requireRecord(config.project, "project", ["description"]);
|
|
60
|
+
if (config.project.description !== undefined) {
|
|
61
|
+
requireNonEmptyString(config.project.description, "project.description");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
22
64
|
if (config.assistant.systemPrompt !== undefined) {
|
|
23
65
|
requireNonEmptyString(config.assistant.systemPrompt, "assistant.systemPrompt");
|
|
24
66
|
}
|
|
25
|
-
if (config.assistant.promptProfile
|
|
67
|
+
if (config.assistant.promptProfile === "custom" && !config.assistant.systemPrompt) {
|
|
68
|
+
throw new Error("assistant.systemPrompt is required for the custom prompt profile");
|
|
69
|
+
}
|
|
70
|
+
if (config.assistant.promptProfile !== undefined && (typeof config.assistant.promptProfile !== "string" || !PROMPT_PROFILES.has(config.assistant.promptProfile))) {
|
|
26
71
|
throw new Error("assistant.promptProfile is not supported");
|
|
27
72
|
}
|
|
28
73
|
if (config.assistant.fallbackMessage !== undefined) {
|
|
29
74
|
requireNonEmptyString(config.assistant.fallbackMessage, "assistant.fallbackMessage");
|
|
30
75
|
}
|
|
31
|
-
|
|
32
|
-
|
|
76
|
+
let sources = [];
|
|
77
|
+
if (config.knowledge !== undefined) {
|
|
78
|
+
requireRecord(config.knowledge, "knowledge", ["sources"]);
|
|
79
|
+
if (!Array.isArray(config.knowledge.sources)) {
|
|
80
|
+
throw new Error("knowledge.sources must be an array");
|
|
81
|
+
}
|
|
82
|
+
if (config.knowledge.sources.length !== 1) {
|
|
83
|
+
throw new Error("knowledge.sources must contain exactly one website source when provided");
|
|
84
|
+
}
|
|
85
|
+
sources = config.knowledge.sources;
|
|
33
86
|
}
|
|
34
|
-
for (const [index,
|
|
87
|
+
for (const [index, sourceValue] of sources.entries()) {
|
|
88
|
+
const sourcePath = `knowledge.sources[${index}]`;
|
|
89
|
+
requireRecord(sourceValue, sourcePath, ["type", "url", "mode", "sitemapUrl", "manualUrls", "include", "exclude", "maxPages", "maxDepth", "autoRescrape", "rescrapeInterval"]);
|
|
90
|
+
const source = sourceValue;
|
|
35
91
|
if (source.type !== "website") {
|
|
36
|
-
throw new Error(
|
|
92
|
+
throw new Error(`${sourcePath}.type is not supported`);
|
|
93
|
+
}
|
|
94
|
+
validateHttpUrl(source.url, `${sourcePath}.url`);
|
|
95
|
+
const maxPages = source.maxPages;
|
|
96
|
+
if (maxPages !== undefined && (typeof maxPages !== "number" || !Number.isInteger(maxPages) || maxPages < 1)) {
|
|
97
|
+
throw new Error(`${sourcePath}.maxPages must be a positive integer`);
|
|
37
98
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const url = new URL(source.url);
|
|
41
|
-
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
42
|
-
throw new Error;
|
|
43
|
-
} catch {
|
|
44
|
-
throw new Error(`knowledge.sources[${index}].url must be an HTTP(S) URL`);
|
|
99
|
+
if (source.mode !== undefined && (typeof source.mode !== "string" || !SOURCE_MODES.has(source.mode))) {
|
|
100
|
+
throw new Error(`${sourcePath}.mode is not supported`);
|
|
45
101
|
}
|
|
46
|
-
if (source.
|
|
47
|
-
|
|
102
|
+
if (source.sitemapUrl !== undefined)
|
|
103
|
+
validateHttpUrl(source.sitemapUrl, `${sourcePath}.sitemapUrl`);
|
|
104
|
+
const manualUrls = source.manualUrls;
|
|
105
|
+
validateOptionalStringArray(manualUrls, `${sourcePath}.manualUrls`);
|
|
106
|
+
for (const [urlIndex, manualUrl] of (manualUrls ?? []).entries()) {
|
|
107
|
+
validateHttpUrl(manualUrl, `${sourcePath}.manualUrls[${urlIndex}]`);
|
|
48
108
|
}
|
|
109
|
+
if (source.mode === "manual" && (!manualUrls || manualUrls.length === 0)) {
|
|
110
|
+
throw new Error(`${sourcePath}.manualUrls must contain at least one URL in manual mode`);
|
|
111
|
+
}
|
|
112
|
+
const maxDepth = source.maxDepth;
|
|
113
|
+
if (maxDepth !== undefined && (typeof maxDepth !== "number" || !Number.isInteger(maxDepth) || maxDepth < 0 || maxDepth > 20)) {
|
|
114
|
+
throw new Error(`${sourcePath}.maxDepth must be an integer between 0 and 20`);
|
|
115
|
+
}
|
|
116
|
+
if (source.autoRescrape !== undefined && typeof source.autoRescrape !== "boolean") {
|
|
117
|
+
throw new Error(`${sourcePath}.autoRescrape must be a boolean`);
|
|
118
|
+
}
|
|
119
|
+
if (source.rescrapeInterval !== undefined && (typeof source.rescrapeInterval !== "string" || !RESCRAPE_INTERVALS.has(source.rescrapeInterval))) {
|
|
120
|
+
throw new Error(`${sourcePath}.rescrapeInterval is not supported`);
|
|
121
|
+
}
|
|
122
|
+
if (source.autoRescrape && !source.rescrapeInterval) {
|
|
123
|
+
throw new Error(`${sourcePath}.rescrapeInterval is required when autoRescrape is enabled`);
|
|
124
|
+
}
|
|
125
|
+
validateOptionalStringArray(source.include, `${sourcePath}.include`);
|
|
126
|
+
validateOptionalStringArray(source.exclude, `${sourcePath}.exclude`);
|
|
49
127
|
}
|
|
50
|
-
if (config.runtime
|
|
51
|
-
|
|
128
|
+
if (config.runtime !== undefined) {
|
|
129
|
+
requireRecord(config.runtime, "runtime", ["locale", "localeMode", "defaultLocale", "allowLocaleOverride"]);
|
|
130
|
+
for (const [path, locale] of [["runtime.locale", config.runtime.locale], ["runtime.defaultLocale", config.runtime.defaultLocale]]) {
|
|
131
|
+
if (locale !== undefined && (typeof locale !== "string" || !WIDGET_LOCALES.has(locale))) {
|
|
132
|
+
throw new Error(`${path} is not supported`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (config.runtime.locale !== undefined && (config.runtime.localeMode !== undefined || config.runtime.defaultLocale !== undefined || config.runtime.allowLocaleOverride === true)) {
|
|
136
|
+
throw new Error("runtime.locale cannot be combined with localeMode, defaultLocale, or an enabled locale override");
|
|
137
|
+
}
|
|
138
|
+
if (config.runtime.localeMode === "fixed" && !config.runtime.defaultLocale) {
|
|
139
|
+
throw new Error("runtime.defaultLocale is required in fixed locale mode");
|
|
140
|
+
}
|
|
141
|
+
if (config.runtime.localeMode !== undefined && config.runtime.localeMode !== "auto" && config.runtime.localeMode !== "fixed") {
|
|
142
|
+
throw new Error("runtime.localeMode is not supported");
|
|
143
|
+
}
|
|
144
|
+
if (config.runtime.allowLocaleOverride !== undefined && typeof config.runtime.allowLocaleOverride !== "boolean") {
|
|
145
|
+
throw new Error("runtime.allowLocaleOverride must be a boolean");
|
|
146
|
+
}
|
|
52
147
|
}
|
|
53
148
|
if (config.widget !== undefined) {
|
|
149
|
+
requireRecord(config.widget, "widget", ["enabled", "position", "headerTitle", "welcomeMessage", "showBranding", "icon", "theme"]);
|
|
54
150
|
if (typeof config.widget.enabled !== "boolean") {
|
|
55
151
|
throw new Error("widget.enabled must be a boolean");
|
|
56
152
|
}
|
|
57
|
-
if (config.widget.
|
|
153
|
+
if (config.widget.showBranding !== undefined && typeof config.widget.showBranding !== "boolean") {
|
|
154
|
+
throw new Error("widget.showBranding must be a boolean");
|
|
155
|
+
}
|
|
156
|
+
if (config.widget.position !== undefined && (typeof config.widget.position !== "string" || !WIDGET_POSITIONS.has(config.widget.position))) {
|
|
58
157
|
throw new Error("widget.position is not supported");
|
|
59
158
|
}
|
|
159
|
+
const icon = config.widget.icon;
|
|
160
|
+
if (icon !== undefined)
|
|
161
|
+
requireRecord(icon, "widget.icon", ["type", "size", "emoji", "url"]);
|
|
162
|
+
if (icon && !ICON_TYPES.has(icon.type))
|
|
163
|
+
throw new Error("widget.icon.type is not supported");
|
|
164
|
+
if (icon?.size !== undefined && (typeof icon.size !== "string" || !ICON_SIZES.has(icon.size))) {
|
|
165
|
+
throw new Error("widget.icon.size is not supported");
|
|
166
|
+
}
|
|
167
|
+
if (icon?.type === "emoji")
|
|
168
|
+
requireNonEmptyString(icon.emoji, "widget.icon.emoji");
|
|
169
|
+
if (icon?.type === "url")
|
|
170
|
+
validateHttpUrl(icon.url, "widget.icon.url");
|
|
171
|
+
for (const field of ["emoji", "url"]) {
|
|
172
|
+
if (icon?.[field] !== undefined && typeof icon[field] !== "string") {
|
|
173
|
+
throw new Error(`widget.icon.${field} must be a string`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (config.widget.headerTitle !== undefined)
|
|
177
|
+
requireNonEmptyString(config.widget.headerTitle, "widget.headerTitle");
|
|
178
|
+
if (config.widget.welcomeMessage !== undefined)
|
|
179
|
+
requireNonEmptyString(config.widget.welcomeMessage, "widget.welcomeMessage");
|
|
180
|
+
if (config.widget.theme !== undefined)
|
|
181
|
+
requireRecord(config.widget.theme, "widget.theme", ["primaryColor", "backgroundColor", "textColor"]);
|
|
60
182
|
}
|
|
61
183
|
validateOptionalColor(config.widget?.theme?.primaryColor, "widget.theme.primaryColor");
|
|
62
184
|
validateOptionalColor(config.widget?.theme?.backgroundColor, "widget.theme.backgroundColor");
|