@paroicms/site-generator-plugin 0.9.0 → 0.9.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paroicms/site-generator-plugin",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "ParoiCMS Site Generator Plugin",
5
5
  "keywords": [
6
6
  "paroicms",
@@ -1,49 +0,0 @@
1
- import { invokeMessageGuard } from "../generator/llm-queries/invoke-message-guard.js";
2
- import { invokeNewSiteAnalysis } from "../generator/llm-queries/invoke-new-site-analysis.js";
3
- import { invokeUpdateSiteSchema } from "../generator/llm-queries/invoke-update-site-schema.js";
4
- import { generateSite } from "../generator/site-generator/site-generator.js";
5
- import { newSession, verifySessionToken } from "./generator-session.js";
6
- export async function executeCommand(commandCtx, input) {
7
- if (input.command === "newSession") {
8
- return {
9
- success: true,
10
- result: await newSession(commandCtx, input),
11
- };
12
- }
13
- const { sessionId } = verifySessionToken(commandCtx, input.sessionToken);
14
- const ctx = {
15
- ...commandCtx,
16
- sessionId,
17
- };
18
- if (input.command === "createSiteSchema") {
19
- const errorResponse = await invokeMessageGuard(ctx, input);
20
- if (errorResponse)
21
- return errorResponse;
22
- return {
23
- success: true,
24
- result: await invokeNewSiteAnalysis(ctx, input),
25
- };
26
- }
27
- if (input.command === "updateSiteSchema") {
28
- const errorResponse = await invokeMessageGuard(ctx, input, {
29
- skipOutOfScopeCheck: true,
30
- });
31
- if (errorResponse)
32
- return errorResponse;
33
- return {
34
- success: true,
35
- result: await invokeUpdateSiteSchema(ctx, input),
36
- };
37
- }
38
- if (input.command === "generateSite") {
39
- return {
40
- success: true,
41
- result: await generateSite(ctx, input),
42
- };
43
- }
44
- ctx.logger.error(`Invalid command: ${input.command}`);
45
- return {
46
- success: false,
47
- userMessage: "Invalid command",
48
- };
49
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Custom API error class for handling API-specific errors
3
- */
4
- export class ApiError extends Error {
5
- statusCode;
6
- constructor(message, statusCode = 400) {
7
- super(message);
8
- this.name = "ApiError";
9
- this.statusCode = statusCode;
10
- }
11
- }
12
- /**
13
- * Create a new API error
14
- * @param message Error message
15
- * @param statusCode HTTP status code (default: 400)
16
- * @returns ApiError instance
17
- */
18
- export function createApiError(message, statusCode = 400) {
19
- return new ApiError(message, statusCode);
20
- }
@@ -1,45 +0,0 @@
1
- import { invokeMessageGuard } from "./llm-queries/invoke-message-guard.js";
2
- import { invokeNewSiteAnalysis } from "./llm-queries/invoke-new-site-analysis.js";
3
- import { invokeUpdateSiteSchema } from "./llm-queries/invoke-update-site-schema.js";
4
- import { newSession, verifySessionToken } from "./session/generator-session.js";
5
- import { generateSite } from "./site-generator/site-generator.js";
6
- export async function executeCommand(ctx, input) {
7
- if (input.command === "newSession") {
8
- return {
9
- success: true,
10
- result: await newSession(ctx, input),
11
- };
12
- }
13
- verifySessionToken(input.sessionToken);
14
- if (input.command === "createSiteSchema") {
15
- const errorResponse = await invokeMessageGuard(ctx, input);
16
- if (errorResponse)
17
- return errorResponse;
18
- return {
19
- success: true,
20
- result: await invokeNewSiteAnalysis(ctx, input),
21
- };
22
- }
23
- if (input.command === "updateSiteSchema") {
24
- const errorResponse = await invokeMessageGuard(ctx, input, {
25
- skipOutOfScopeCheck: true,
26
- });
27
- if (errorResponse)
28
- return errorResponse;
29
- return {
30
- success: true,
31
- result: await invokeUpdateSiteSchema(ctx, input),
32
- };
33
- }
34
- if (input.command === "generateSite") {
35
- return {
36
- success: true,
37
- result: await generateSite(ctx, input),
38
- };
39
- }
40
- ctx.logger.error(`Invalid command: ${input.command}`);
41
- return {
42
- success: false,
43
- userMessage: "Invalid command",
44
- };
45
- }
@@ -1,51 +0,0 @@
1
- import { getRandomImagePath } from "../lib/images-lib.js";
2
- export function augmentWithComputedValues(list, nodeType, language) {
3
- if (!nodeType.fields)
4
- return;
5
- for (const item of list) {
6
- for (const f of nodeType.fields) {
7
- const languageKey = f.storedOn === "node" ? "." : language;
8
- if (f.storedAs === "mediaHandle") {
9
- const value = getMediaGeneratedFieldContent(f, language);
10
- if (value) {
11
- item[f.name] = value;
12
- }
13
- }
14
- else {
15
- const value = getDefaultValueForField(f.name);
16
- if (value !== undefined) {
17
- item[f.name] = { [languageKey]: value };
18
- }
19
- }
20
- }
21
- }
22
- }
23
- function getMediaGeneratedFieldContent(f, language) {
24
- const languageKey = f.storedOn === "node" ? "." : language;
25
- if (f.dataType === "media") {
26
- return { [languageKey]: { file: getRandomImagePath() } };
27
- }
28
- if (f.dataType === "gallery") {
29
- return {
30
- [languageKey]: {
31
- files: [
32
- getRandomImagePath(),
33
- getRandomImagePath(),
34
- getRandomImagePath(),
35
- getRandomImagePath(),
36
- getRandomImagePath(),
37
- getRandomImagePath(),
38
- ],
39
- },
40
- };
41
- }
42
- }
43
- function getDefaultValueForField(fieldName) {
44
- if (fieldName === "phone" || fieldName === "phone2")
45
- return "0123456789";
46
- if (fieldName === "phones")
47
- return JSON.stringify(["0123456789", "9876543210"]);
48
- if (fieldName === "updateDateTime")
49
- return new Date().toISOString();
50
- return undefined;
51
- }
@@ -1,33 +0,0 @@
1
- import jwt from "jsonwebtoken";
2
- import { createApiError } from "../errors.js";
3
- const JWT_SECRET = "init"; // Hardcoded JWT secret as specified
4
- /**
5
- * Verifies a session token and returns the session ID if valid
6
- * @param token The JWT token to verify
7
- * @returns The session ID contained in the token
8
- * @throws ApiError if the token is invalid or expired
9
- */
10
- export function verifySessionToken(token) {
11
- try {
12
- // Verify and decode the token
13
- const payload = jwt.verify(token, JWT_SECRET);
14
- if (!payload || !payload.sessionId) {
15
- throw createApiError("Invalid session token", 401);
16
- }
17
- return payload.sessionId;
18
- }
19
- catch (error) {
20
- if (error.name === "TokenExpiredError") {
21
- throw createApiError("Session token expired", 401);
22
- }
23
- else if (error.name === "JsonWebTokenError") {
24
- throw createApiError("Invalid session token", 401);
25
- }
26
- // Re-throw our custom error
27
- if (error.name === "ApiError") {
28
- throw error;
29
- }
30
- // Unexpected error
31
- throw createApiError("Session verification failed", 500);
32
- }
33
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,118 +0,0 @@
1
- // Default limits if not specified in configuration
2
- const DEFAULT_REQUEST_TOKENS = 8000;
3
- const DEFAULT_SESSION_TOKENS = 32000;
4
- const DEFAULT_DAILY_TOKENS = 100000;
5
- /**
6
- * Initialize token tracking for a session
7
- */
8
- export function initTokenTracking(pluginConf) {
9
- const requestLimit = pluginConf.maxRequestTokens ?? DEFAULT_REQUEST_TOKENS;
10
- const sessionLimit = pluginConf.maxSessionTokens ?? DEFAULT_SESSION_TOKENS;
11
- return {
12
- requestLimit,
13
- sessionLimit,
14
- sessionUsage: 0,
15
- userLimits: new Map(),
16
- };
17
- }
18
- /**
19
- * Roughly estimate token count from text length
20
- * This is a simple approximation - for production, use a proper tokenizer
21
- */
22
- export function estimateTokenCount(text) {
23
- // Rough estimate: 1 token ≈ 4 characters for English text
24
- return Math.ceil(text.length / 4);
25
- }
26
- /**
27
- * Track token usage for the current user
28
- */
29
- export function trackUserTokens(ctx, tokenCount) {
30
- const { userKey, tokenUsage, logger } = ctx;
31
- const dailyLimit = ctx.pluginConf.maxDailyTokens ?? DEFAULT_DAILY_TOKENS;
32
- // Update session usage
33
- tokenUsage.sessionUsage += tokenCount;
34
- // Get or create user usage entry
35
- let userUsage = tokenUsage.userLimits.get(userKey);
36
- if (!userUsage) {
37
- userUsage = {
38
- dailyUsage: 0,
39
- lastReset: new Date(),
40
- totalRequests: 0,
41
- };
42
- tokenUsage.userLimits.set(userKey, userUsage);
43
- }
44
- // Reset daily usage if it's a new day
45
- const today = new Date().toDateString();
46
- if (userUsage.lastReset.toDateString() !== today) {
47
- logger.info(`Resetting daily token usage for user ${userKey}`);
48
- userUsage.dailyUsage = 0;
49
- userUsage.lastReset = new Date();
50
- }
51
- // Update daily usage
52
- userUsage.dailyUsage += tokenCount;
53
- userUsage.totalRequests++;
54
- // Log usage
55
- logger.debug(`Token usage - User: ${userKey}, Request: ${tokenCount}, ` +
56
- `Session: ${tokenUsage.sessionUsage}/${tokenUsage.sessionLimit}, ` +
57
- `Daily: ${userUsage.dailyUsage}/${dailyLimit}`);
58
- }
59
- /**
60
- * Check if token limits have been exceeded
61
- * @throws Error if any limit is exceeded
62
- */
63
- export function checkTokenLimits(ctx, estimatedTokens) {
64
- const { tokenUsage, userKey, logger } = ctx;
65
- const dailyLimit = ctx.pluginConf.maxDailyTokens ?? DEFAULT_DAILY_TOKENS;
66
- // Check single request limit
67
- if (estimatedTokens > tokenUsage.requestLimit) {
68
- logger.warn(`User ${userKey} exceeded request token limit: ${estimatedTokens}/${tokenUsage.requestLimit}`);
69
- throw new Error(`Prompt exceeds maximum token limit of ${tokenUsage.requestLimit}`);
70
- }
71
- // Check session limit
72
- if (tokenUsage.sessionUsage + estimatedTokens > tokenUsage.sessionLimit) {
73
- logger.warn(`User ${userKey} exceeded session token limit: ${tokenUsage.sessionUsage}/${tokenUsage.sessionLimit}`);
74
- throw new Error("Session token limit exceeded. Please start a new session.");
75
- }
76
- // Check daily limit
77
- const userUsage = tokenUsage.userLimits.get(userKey);
78
- if (userUsage && userUsage.dailyUsage + estimatedTokens > dailyLimit) {
79
- logger.warn(`User ${userKey} exceeded daily token limit: ${userUsage.dailyUsage}/${dailyLimit}`);
80
- throw new Error("Daily token limit exceeded. Please try again tomorrow.");
81
- }
82
- }
83
- /**
84
- * Safe wrapper for LLM invocation with token tracking and limits
85
- */
86
- export async function invokeWithTokenLimit(ctx, model, prompt, processor) {
87
- // For text prompts, estimate tokens
88
- let promptTokens = 0;
89
- if (typeof prompt === "string") {
90
- promptTokens = estimateTokenCount(prompt);
91
- }
92
- else if (prompt.message && typeof prompt.message === "string") {
93
- promptTokens = estimateTokenCount(prompt.message);
94
- }
95
- else {
96
- // JSON objects are harder to estimate; use a conservative approach
97
- promptTokens = estimateTokenCount(JSON.stringify(prompt)) * 1.5;
98
- }
99
- // Check limits before making the call
100
- checkTokenLimits(ctx, promptTokens);
101
- // Make the LLM call
102
- const response = await model.invoke(prompt);
103
- // Estimate response tokens and track total
104
- let responseContent = "";
105
- if (typeof response.content === "string") {
106
- responseContent = response.content;
107
- }
108
- else if (response.content && response.content.length > 0) {
109
- // Handle array response
110
- responseContent = response.content.join("\n");
111
- }
112
- const responseTokens = estimateTokenCount(responseContent);
113
- const totalTokens = promptTokens + responseTokens;
114
- // Track the usage
115
- trackUserTokens(ctx, totalTokens);
116
- // Process and return the response
117
- return processor(response);
118
- }
@@ -1,33 +0,0 @@
1
- import { ApiError } from "@paroicms/public-server-lib";
2
- import { nanoid } from "nanoid";
3
- const { sign, verify } = (await import("jsonwebtoken")).default;
4
- const JWT_SECRET = "init"; // FIXME: Hardcoded JWT secret as specified
5
- export async function newSession(ctx, command) {
6
- const { service } = ctx;
7
- const { recaptchaToken } = command;
8
- const isValid = await service.validateRecaptchaResponse(recaptchaToken);
9
- if (!isValid)
10
- throw new ApiError("Invalid reCAPTCHA token", 400);
11
- const sessionId = nanoid();
12
- const token = sign({ sessionId }, JWT_SECRET, { expiresIn: "24h" });
13
- return { token };
14
- }
15
- /**
16
- * Verifies a session token and returns the session ID if valid
17
- */
18
- export function verifySessionToken(token) {
19
- let payload;
20
- try {
21
- payload = verify(token, JWT_SECRET);
22
- }
23
- catch (error) {
24
- if (error.name === "TokenExpiredError")
25
- throw new ApiError("Session token expired", 401);
26
- if (error.name === "JsonWebTokenError")
27
- throw new ApiError("Invalid session token", 401);
28
- throw error;
29
- }
30
- if (!payload || !payload.sessionId)
31
- throw new ApiError("Invalid session token", 401);
32
- return payload.sessionId;
33
- }
@@ -1,17 +0,0 @@
1
- import { ApiError } from "@paroicms/public-server-lib";
2
- import { nanoid } from "nanoid";
3
- const { sign, verify } = (await import("jsonwebtoken")).default;
4
- export async function newSession(ctx, command) {
5
- const { service } = ctx;
6
- const { recaptchaToken } = command;
7
- const isValid = await service.validateRecaptchaResponse(recaptchaToken);
8
- if (!isValid) {
9
- throw new ApiError("Invalid reCAPTCHA token", {
10
- status: 400,
11
- });
12
- }
13
- const sessionId = nanoid();
14
- const token = sign({ sessionId }, "init", // Hardcoded JWT secret as specified
15
- { expiresIn: "1h" });
16
- return { token };
17
- }
@@ -1,262 +0,0 @@
1
- export function getThemeCssContent() {
2
- return `/* Reset */
3
- * {
4
- box-sizing: border-box;
5
- }
6
-
7
- a,
8
- a:visited {
9
- color: inherit;
10
- text-decoration: none;
11
- }
12
-
13
- /* Elements */
14
-
15
- body {
16
- background-color: #aaa;
17
- margin: 0;
18
- padding: 0;
19
- }
20
-
21
- img {
22
- display: block;
23
- max-width: 100%;
24
- }
25
-
26
- picture {
27
- display: block;
28
- }
29
-
30
- /* Tile */
31
-
32
- a > article {
33
- background-color: #fff;
34
- border-radius: 6px;
35
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
36
- display: flex;
37
- max-width: 340px;
38
- overflow: hidden;
39
- }
40
- a > article div:first-child {
41
- flex: 0 0 120px;
42
- max-width: 120px;
43
- }
44
- a > article img {
45
- height: 100%;
46
- object-fit: cover;
47
- width: 100%;
48
- }
49
- a > article div:last-child {
50
- display: flex;
51
- flex: 1;
52
- flex-direction: column;
53
- padding: 15px;
54
- }
55
- a > article h3 {
56
- margin: 0 0 8px;
57
- font-size: 16px;
58
- font-weight: bold;
59
- color: #333;
60
- }
61
- a > article p {
62
- color: #666;
63
- font-size: 14px;
64
- line-height: 1.4;
65
- margin: 0;
66
- }
67
-
68
- a:hover > article {
69
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
70
- }
71
-
72
- /* Classes */
73
-
74
- ._bg2 {
75
- background-color: #444;
76
- color: #eee;
77
- }
78
-
79
- .Container {
80
- max-width: 1200px;
81
- margin: 0 auto;
82
- padding: 0 20px;
83
- }
84
-
85
- .Page {
86
- background-color: #fff;
87
- color: #333;
88
- padding: 5px 40px;
89
- }
90
-
91
- .TextWidth {
92
- max-width: 600px;
93
- margin: 0 auto;
94
- }
95
-
96
- .List {
97
- display: flex;
98
- gap: 20px;
99
- flex-wrap: wrap;
100
- }
101
-
102
- .Header {
103
- display: flex;
104
- align-items: center;
105
- justify-content: space-between;
106
- padding-bottom: 15px;
107
- padding-top: 15px;
108
- color: #fff;
109
- }
110
-
111
- @media (max-width: 768px) {
112
- .Header {
113
- flex-wrap: wrap;
114
- padding: 15px 0 5px;
115
- }
116
- }
117
-
118
- .Header-logo {
119
- display: flex;
120
- align-items: center;
121
- }
122
-
123
- .Header-logo img {
124
- margin-right: 10px;
125
- border-radius: 50%;
126
- }
127
-
128
- .Header-title {
129
- font-size: 20px;
130
- font-weight: bold;
131
- }
132
-
133
- .Header-search {
134
- padding: 0 10px;
135
- }
136
-
137
- .Text::after {
138
- clear: both;
139
- content: "";
140
- display: block;
141
- }
142
-
143
- .Text .Img {
144
- height: auto;
145
- }
146
-
147
- .Text .Img.left {
148
- float: left;
149
- margin: 5px 20px 10px 0;
150
- }
151
-
152
- .Text .Img.right {
153
- float: right;
154
- margin: 5px 0 10px 20px;
155
- }
156
-
157
- .Text .Img.left, .Text .Img.right {
158
- max-width: 50%;
159
- }
160
-
161
- .Text .Img.center {
162
- display: block;
163
- margin: 20px auto;
164
- }
165
-
166
- .Text a,
167
- .Text a:visited,
168
- .TextLink,
169
- .TextLink:visited {
170
- color: #007bff;
171
- cursor: pointer;
172
- text-decoration: underline;
173
- }
174
-
175
- .Hero img {
176
- height: auto;
177
- width: 100%;
178
- }
179
-
180
- .InfiniteLoading-actionArea {
181
- align-items: center;
182
- display: flex;
183
- justify-content: center;
184
- }
185
-
186
- .Button,
187
- .SearchOpenerBtn,
188
- .InfiniteLoading-btn {
189
- background-color: #3498db;
190
- border: 1px solid #3498db;
191
- border-radius: 5px;
192
- color: #fff;
193
- cursor: pointer;
194
- display: inline-block;
195
- font-size: 16px;
196
- margin: 10px 0;
197
- padding: 10px 20px;
198
- text-align: center;
199
- text-decoration: none;
200
- }
201
- .Button:hover,
202
- .SearchOpenerBtn:hover,
203
- .InfiniteLoading-btn:hover {
204
- background-color: #2980b9;
205
- border-color: #2980b9;
206
- }
207
-
208
- .SearchOpenerBtn {
209
- display: flex;
210
- }
211
-
212
- .Navbar {
213
- align-items: center;
214
- display: flex;
215
- flex-grow: 1;
216
- justify-content: center;
217
- margin: 0 15px;
218
- }
219
-
220
- @media (max-width: 768px) {
221
- .Navbar {
222
- flex-basis: 100%;
223
- order: 3;
224
- margin: 10px 0 5px;
225
- justify-content: flex-start;
226
- overflow-x: auto;
227
- padding-bottom: 5px;
228
- }
229
- }
230
-
231
- .NavButton {
232
- color: rgba(255, 255, 255, 0.85);
233
- padding: 8px;
234
- margin: 0 4px;
235
- border-radius: 4px;
236
- font-size: 17px;
237
- font-weight: bold;
238
- position: relative;
239
- transition: all 0.2s ease;
240
- }
241
-
242
- .NavButton:hover {
243
- color: #fff;
244
- background-color: rgba(255, 255, 255, 0.1);
245
- }
246
-
247
- .NavButton.active {
248
- color: #fff;
249
- }
250
-
251
- .NavButton.active::after {
252
- content: "";
253
- position: absolute;
254
- bottom: 0;
255
- left: 16px;
256
- right: 16px;
257
- height: 3px;
258
- background-color: #3498db;
259
- border-radius: 1.5px;
260
- }
261
- `;
262
- }
@@ -1,14 +0,0 @@
1
- export function createCommandContext(service, pluginConf, rawContext) {
2
- const packConf = service.connector.getSitePackConf(pluginConf.packName);
3
- const { sitesDir, packName } = packConf;
4
- if (!sitesDir) {
5
- throw new Error(`Site-generator plugin can generate sites only for pack with "sitesDir", but pack "${packName}" doesn't have it`);
6
- }
7
- return {
8
- ...rawContext,
9
- sitesDir,
10
- packName,
11
- service,
12
- logger: service.logger,
13
- };
14
- }