autoblogger 0.1.15 → 0.1.17

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/index.js CHANGED
@@ -31,49 +31,140 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // src/ai/prompts.ts
34
- var DEFAULT_GENERATE_TEMPLATE, DEFAULT_CHAT_TEMPLATE, DEFAULT_REWRITE_TEMPLATE, DEFAULT_AUTO_DRAFT_TEMPLATE, DEFAULT_PLAN_TEMPLATE, DEFAULT_PLAN_RULES, DEFAULT_EXPAND_PLAN_TEMPLATE;
34
+ var DEFAULT_GENERATE_TEMPLATE, DEFAULT_CHAT_TEMPLATE, DEFAULT_REWRITE_TEMPLATE, DEFAULT_AUTO_DRAFT_TEMPLATE, DEFAULT_PLAN_TEMPLATE, DEFAULT_PLAN_RULES, DEFAULT_AGENT_TEMPLATE, DEFAULT_EXPAND_PLAN_TEMPLATE;
35
35
  var init_prompts = __esm({
36
36
  "src/ai/prompts.ts"() {
37
37
  "use strict";
38
- DEFAULT_GENERATE_TEMPLATE = `You are an expert essay writer. Write engaging, thoughtful content.
38
+ DEFAULT_GENERATE_TEMPLATE = `<system>
39
+ <role>Expert essay writer creating engaging, thoughtful content</role>
39
40
 
41
+ <critical>
42
+ ALWAYS output a complete essay. NEVER respond conversationally.
43
+ - Do NOT ask questions or request clarification
44
+ - Do NOT say "Here is your essay" or similar preamble
45
+ - Do NOT explain what you're going to write
46
+ - If the prompt is vague, make creative choices and proceed
47
+ - Output ONLY the essay in markdown format
48
+ </critical>
49
+
50
+ <rules>
40
51
  {{RULES}}
52
+ </rules>
53
+
54
+ <constraints>
55
+ <word_count>{{WORD_COUNT}}</word_count>
56
+ </constraints>
41
57
 
42
- Write approximately {{WORD_COUNT}} words.
58
+ <output_format>
59
+ CRITICAL: Your response MUST start with exactly this format:
43
60
 
44
- IMPORTANT: Start your response with exactly this format:
45
- # Title Here
46
- *Subtitle here*
61
+ Line 1: # [Your Title Here]
62
+ Line 2: *[Your subtitle here]*
63
+ Line 3: (blank line)
64
+ Line 4+: Essay body in markdown
47
65
 
48
- Then write the essay body. The title must be on line 1 with a # prefix. The subtitle must be on line 2 wrapped in asterisks (*like this*).`;
49
- DEFAULT_CHAT_TEMPLATE = `You are a helpful writing assistant.
66
+ <title_guidelines>
67
+ - Be SPECIFIC, not generic (avoid "The Power of", "Why X Matters", "A Guide to")
68
+ - Include a concrete detail, angle, or unexpected element
69
+ - Create curiosity or make a bold claim
70
+ - 5-12 words ideal
71
+ </title_guidelines>
50
72
 
73
+ <subtitle_guidelines>
74
+ - One sentence that hooks the reader
75
+ - Tease the main argument or reveal a key insight
76
+ - Create tension, curiosity, or promise value
77
+ - Make readers want to continue reading
78
+ </subtitle_guidelines>
79
+ </output_format>
80
+ </system>`;
81
+ DEFAULT_CHAT_TEMPLATE = `<system>
82
+ <role>Helpful writing assistant for essay creation and editing</role>
83
+
84
+ <rules>
51
85
  {{CHAT_RULES}}
86
+ </rules>
87
+
88
+ <context>
89
+ {{ESSAY_CONTEXT}}
90
+ </context>
52
91
 
53
- {{ESSAY_CONTEXT}}`;
54
- DEFAULT_REWRITE_TEMPLATE = `You are a writing assistant that improves text.
92
+ <behavior>
93
+ - Be concise and actionable
94
+ - When suggesting edits, be specific about what to change
95
+ - Match the author's voice and style when writing
96
+ - Ask clarifying questions if the request is ambiguous
97
+ </behavior>
98
+ </system>`;
99
+ DEFAULT_REWRITE_TEMPLATE = `<system>
100
+ <role>Writing assistant that improves text quality</role>
55
101
 
102
+ <rules>
56
103
  {{REWRITE_RULES}}
104
+ </rules>
57
105
 
58
- Keep the same meaning. Improve clarity and flow.`;
59
- DEFAULT_AUTO_DRAFT_TEMPLATE = `You are an expert essay writer. Write an engaging essay based on the news article.
106
+ <behavior>
107
+ - Preserve the original meaning exactly
108
+ - Improve clarity, flow, and readability
109
+ - Fix grammar and punctuation issues
110
+ - Maintain the author's voice and tone
111
+ - Output only the improved text, no explanations
112
+ </behavior>
113
+ </system>`;
114
+ DEFAULT_AUTO_DRAFT_TEMPLATE = `<system>
115
+ <role>Expert essay writer creating engaging content from news articles</role>
60
116
 
117
+ <auto_draft_rules>
61
118
  {{AUTO_DRAFT_RULES}}
119
+ </auto_draft_rules>
62
120
 
121
+ <writing_rules>
63
122
  {{RULES}}
123
+ </writing_rules>
64
124
 
65
- Write approximately {{AUTO_DRAFT_WORD_COUNT}} words.`;
66
- DEFAULT_PLAN_TEMPLATE = `You are a writing assistant that outputs essay outlines.
125
+ <constraints>
126
+ <word_count>{{AUTO_DRAFT_WORD_COUNT}}</word_count>
127
+ </constraints>
67
128
 
68
- Wrap your entire response in <plan> tags. Output nothing outside the tags.
129
+ <output_format>
130
+ CRITICAL: Your response MUST start with exactly this format:
69
131
 
70
- {{PLAN_RULES}}
132
+ Line 1: # [Your Title Here]
133
+ Line 2: *[Your subtitle here]*
134
+ Line 3: (blank line)
135
+ Line 4+: Essay body in markdown
136
+
137
+ <title_guidelines>
138
+ - Be SPECIFIC about the news angle, not generic
139
+ - Include a concrete detail or unexpected element
140
+ - Create curiosity or make a bold claim
141
+ - 5-12 words ideal
142
+ </title_guidelines>
143
+
144
+ <subtitle_guidelines>
145
+ - One sentence that hooks the reader
146
+ - Tease the main argument or unique perspective
147
+ - Create tension, curiosity, or promise value
148
+ </subtitle_guidelines>
149
+ </output_format>
150
+ </system>`;
151
+ DEFAULT_PLAN_TEMPLATE = `<system>
152
+ <role>Writing assistant that creates essay outlines</role>
71
153
 
72
- ## Style Reference
73
- {{STYLE_EXAMPLES}}`;
74
- DEFAULT_PLAN_RULES = `STRICT LIMIT: Maximum 3 bullets per section. Most sections should have 1-2 bullets.
154
+ <critical>
155
+ Wrap your ENTIRE response in <plan> tags. Output NOTHING outside the tags.
156
+ </critical>
75
157
 
76
- Output format:
158
+ <rules>
159
+ {{PLAN_RULES}}
160
+ </rules>
161
+
162
+ <style_reference>
163
+ {{STYLE_EXAMPLES}}
164
+ </style_reference>
165
+ </system>`;
166
+ DEFAULT_PLAN_RULES = `<format>
167
+ STRICT LIMIT: Maximum 3 bullets per section. Most sections should have 1-2 bullets.
77
168
 
78
169
  <plan>
79
170
  # Essay Title
@@ -89,34 +180,98 @@ Output format:
89
180
  ## Section Name
90
181
  - Key point
91
182
  </plan>
183
+ </format>
92
184
 
93
- Constraints:
185
+ <constraints>
94
186
  - 4-6 section headings (## lines)
95
187
  - 1-3 bullets per section \u2014 NEVER 4 or more
96
188
  - Bullets are short phrases, not sentences
97
189
  - No prose, no paragraphs, no explanations
98
- - When revising, output the complete updated plan`;
99
- DEFAULT_EXPAND_PLAN_TEMPLATE = `You are a writing assistant that expands essay outlines into full drafts.
190
+ - When revising, output the complete updated plan
191
+ </constraints>
192
+
193
+ <title_guidelines>
194
+ - Be SPECIFIC about the essay's angle
195
+ - Include a concrete detail or unexpected element
196
+ - Avoid generic patterns like "The Power of", "Why X Matters"
197
+ - 5-12 words ideal
198
+ </title_guidelines>
199
+
200
+ <subtitle_guidelines>
201
+ - One sentence that previews the main argument
202
+ - Create curiosity or make a bold claim
203
+ </subtitle_guidelines>`;
204
+ DEFAULT_AGENT_TEMPLATE = `<agent_mode>
205
+ You are in AGENT MODE - you can directly edit the essay. Wrap edits in :::edit and ::: tags with a JSON object.
206
+
207
+ EDIT COMMANDS (use valid JSON):
208
+
209
+ 1. Replace specific text:
210
+ :::edit
211
+ {"type": "replace_section", "find": "exact text to find", "replace": "replacement text"}
212
+ :::
100
213
 
101
- ## Writing Rules (Follow these exactly)
214
+ 2. Replace entire essay:
215
+ :::edit
216
+ {"type": "replace_all", "title": "New Title", "subtitle": "New subtitle", "markdown": "Full essay content..."}
217
+ :::
218
+
219
+ 3. Insert text:
220
+ :::edit
221
+ {"type": "insert", "position": "after", "find": "text to find", "replace": "text to insert"}
222
+ :::
223
+ (position can be: "before", "after", "start", "end")
224
+
225
+ 4. Delete text:
226
+ :::edit
227
+ {"type": "delete", "find": "text to delete"}
228
+ :::
229
+
230
+ RULES:
231
+ - Use EXACT text matches for "find" - copy precisely from the essay
232
+ - One edit block per change
233
+ - You can include multiple edit blocks in one response
234
+ - Add brief explanation before/after edit blocks
235
+ - Edits are applied automatically - the user will see the changes
236
+ </agent_mode>`;
237
+ DEFAULT_EXPAND_PLAN_TEMPLATE = `<system>
238
+ <role>Writing assistant that expands essay outlines into full drafts</role>
239
+
240
+ <writing_rules>
102
241
  {{RULES}}
242
+ </writing_rules>
103
243
 
104
- ## Style Reference (Write in this voice)
244
+ <style_reference>
105
245
  {{STYLE_EXAMPLES}}
246
+ </style_reference>
106
247
 
107
- ---
248
+ <plan_to_expand>
249
+ {{PLAN}}
250
+ </plan_to_expand>
108
251
 
109
- Write an essay following this exact structure:
252
+ <output_format>
253
+ CRITICAL: Your response MUST start with exactly this format:
110
254
 
111
- {{PLAN}}
255
+ Line 1: # [Title from plan, refined if needed]
256
+ Line 2: *[Subtitle from plan, refined if needed]*
257
+ Line 3: (blank line)
258
+ Line 4+: Essay body with ## section headings
112
259
 
113
- Rules:
114
- - Start with the title on line 1 as: # Title Here
115
- - Follow with the subtitle on line 2 as: *Subtitle here*
116
- - Use the section headers as H2 headings
260
+ <requirements>
261
+ - Use the section headers from the plan as H2 headings
117
262
  - Expand each section's bullet points into full paragraphs
118
263
  - Match the author's voice and style from the examples
119
- - Output ONLY markdown. No preamble, no "Here is...", no explanations. Just the essay content.`;
264
+ - Output ONLY markdown \u2014 no preamble, no "Here is...", no explanations
265
+ </requirements>
266
+
267
+ <title_refinement>
268
+ If the plan title is generic, improve it to be:
269
+ - More specific and concrete
270
+ - Curiosity-inducing or bold
271
+ - 5-12 words
272
+ </title_refinement>
273
+ </output_format>
274
+ </system>`;
120
275
  }
121
276
  });
122
277
 
@@ -165,69 +320,212 @@ var init_models = __esm({
165
320
  });
166
321
 
167
322
  // src/ai/provider.ts
323
+ var provider_exports = {};
324
+ __export(provider_exports, {
325
+ createStream: () => createStream
326
+ });
327
+ async function fetchSearchResults(query, openaiKey) {
328
+ try {
329
+ console.log("[Web Search] Fetching search results for:", query.slice(0, 100));
330
+ const openai = new import_openai.default({
331
+ ...openaiKey && { apiKey: openaiKey }
332
+ });
333
+ const response = await openai.responses.create({
334
+ model: "gpt-5-mini",
335
+ input: `You are a research assistant. Provide a concise summary of the most relevant and recent information from the web about the following query. Include key facts, dates, and sources when available. Keep your response under 500 words.
336
+
337
+ Query: ${query}`,
338
+ tools: [{ type: "web_search" }]
339
+ });
340
+ const result = response.output_text || null;
341
+ console.log("[Web Search] Got results:", result ? `${result.length} chars` : "null");
342
+ return result;
343
+ } catch (error) {
344
+ console.error("[Web Search] Failed:", error);
345
+ return null;
346
+ }
347
+ }
348
+ function extractSearchQuery(messages) {
349
+ const userMessages = messages.filter((m) => m.role === "user");
350
+ return userMessages[userMessages.length - 1]?.content || "";
351
+ }
168
352
  async function createStream(options) {
169
353
  const modelConfig = getModel(options.model);
170
354
  if (!modelConfig) {
171
355
  throw new Error(`Unknown model: ${options.model}`);
172
356
  }
173
- if (modelConfig.provider === "anthropic") {
174
- if (!options.anthropicKey) {
175
- throw new Error("Anthropic API key not configured");
357
+ let searchContext = "";
358
+ if (options.useWebSearch && modelConfig.provider === "anthropic") {
359
+ const query = extractSearchQuery(options.messages);
360
+ if (query) {
361
+ const searchResults = await fetchSearchResults(query, options.openaiKey);
362
+ if (searchResults) {
363
+ searchContext = `
364
+
365
+ <web_search_results>
366
+ ${searchResults}
367
+ </web_search_results>
368
+
369
+ Use the search results above to inform your response with current, accurate information.`;
370
+ }
176
371
  }
177
- return createAnthropicStream(options, modelConfig.modelId);
372
+ }
373
+ if (modelConfig.provider === "anthropic") {
374
+ return createAnthropicStream(options, modelConfig.modelId, searchContext);
178
375
  } else {
179
- if (!options.openaiKey) {
180
- throw new Error("OpenAI API key not configured");
181
- }
182
- return createOpenAIStream(options, modelConfig.modelId);
376
+ return createOpenAIStream(options, modelConfig.modelId, options.useWebSearch);
183
377
  }
184
378
  }
185
- async function createAnthropicStream(options, modelId) {
186
- const anthropic = new import_sdk.default({ apiKey: options.anthropicKey });
187
- const systemMessage = options.messages.find((m) => m.role === "system")?.content || "";
379
+ async function createAnthropicStream(options, modelId, searchContext = "") {
380
+ const anthropic = new import_sdk.default({
381
+ ...options.anthropicKey && { apiKey: options.anthropicKey }
382
+ });
383
+ const systemMessage = (options.messages.find((m) => m.role === "system")?.content || "") + searchContext;
188
384
  const chatMessages = options.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }));
189
- const stream = await anthropic.messages.stream({
385
+ const requestParams = {
190
386
  model: modelId,
191
387
  max_tokens: options.maxTokens || 4096,
192
388
  system: systemMessage,
193
389
  messages: chatMessages
194
- });
195
- return new ReadableStream({
196
- async start(controller) {
197
- for await (const event of stream) {
198
- if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
199
- controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ text: event.delta.text })}
390
+ };
391
+ if (options.useThinking && (modelId.includes("claude-sonnet") || modelId.includes("claude-opus"))) {
392
+ requestParams.thinking = {
393
+ type: "enabled",
394
+ budget_tokens: 1e4
395
+ };
396
+ requestParams.max_tokens = Math.max(requestParams.max_tokens, 16e3);
397
+ }
398
+ try {
399
+ const stream = await anthropic.messages.stream(requestParams);
400
+ return new ReadableStream({
401
+ async start(controller) {
402
+ try {
403
+ for await (const event of stream) {
404
+ if (event.type === "content_block_delta") {
405
+ const delta = event.delta;
406
+ if (delta.type === "text_delta" && delta.text) {
407
+ controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ text: delta.text })}
408
+
409
+ `));
410
+ } else if (delta.type === "thinking_delta" && delta.thinking) {
411
+ controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ thinking: delta.thinking })}
412
+
413
+ `));
414
+ }
415
+ }
416
+ }
417
+ controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
418
+ controller.close();
419
+ } catch (streamError) {
420
+ const errorMessage = streamError instanceof Error ? streamError.message : "Stream error";
421
+ console.error("[Anthropic Stream Error]", streamError);
422
+ controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ error: errorMessage })}
200
423
 
201
424
  `));
425
+ controller.close();
202
426
  }
203
427
  }
204
- controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
205
- controller.close();
206
- }
207
- });
428
+ });
429
+ } catch (error) {
430
+ const errorMessage = error instanceof Error ? error.message : "Anthropic API error";
431
+ console.error("[Anthropic API Error]", error);
432
+ throw new Error(errorMessage);
433
+ }
208
434
  }
209
- async function createOpenAIStream(options, modelId) {
210
- const openai = new import_openai.default({ apiKey: options.openaiKey });
211
- const stream = await openai.chat.completions.create({
435
+ async function createOpenAIStream(options, modelId, useWebSearch = false) {
436
+ const openai = new import_openai.default({
437
+ ...options.openaiKey && { apiKey: options.openaiKey }
438
+ });
439
+ if (useWebSearch) {
440
+ return createOpenAIResponsesStream(openai, options, modelId);
441
+ }
442
+ const requestParams = {
212
443
  model: modelId,
213
444
  messages: options.messages,
214
- max_tokens: options.maxTokens || 4096,
445
+ max_completion_tokens: options.maxTokens || 4096,
215
446
  stream: true
216
- });
217
- return new ReadableStream({
218
- async start(controller) {
219
- for await (const chunk of stream) {
220
- const text = chunk.choices[0]?.delta?.content;
221
- if (text) {
222
- controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ text })}
447
+ };
448
+ try {
449
+ const stream = await openai.chat.completions.create(requestParams);
450
+ return new ReadableStream({
451
+ async start(controller) {
452
+ try {
453
+ for await (const chunk of stream) {
454
+ const text = chunk.choices[0]?.delta?.content;
455
+ if (text) {
456
+ controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ text })}
223
457
 
224
458
  `));
459
+ }
460
+ }
461
+ controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
462
+ controller.close();
463
+ } catch (streamError) {
464
+ const errorMessage = streamError instanceof Error ? streamError.message : "Stream error";
465
+ console.error("[OpenAI Stream Error]", streamError);
466
+ controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ error: errorMessage })}
467
+
468
+ `));
469
+ controller.close();
225
470
  }
226
471
  }
227
- controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
228
- controller.close();
229
- }
230
- });
472
+ });
473
+ } catch (error) {
474
+ const errorMessage = error instanceof Error ? error.message : "OpenAI API error";
475
+ console.error("[OpenAI API Error]", error);
476
+ throw new Error(errorMessage);
477
+ }
478
+ }
479
+ async function createOpenAIResponsesStream(openai, options, modelId) {
480
+ const systemMessage = options.messages.find((m) => m.role === "system")?.content || "";
481
+ const conversationMessages = options.messages.filter((m) => m.role !== "system");
482
+ const lastUserMessage = conversationMessages[conversationMessages.length - 1]?.content || "";
483
+ const conversationContext = conversationMessages.slice(0, -1).map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n\n");
484
+ const fullInput = conversationContext ? `${systemMessage}
485
+
486
+ Previous conversation:
487
+ ${conversationContext}
488
+
489
+ User: ${lastUserMessage}` : `${systemMessage}
490
+
491
+ ${lastUserMessage}`;
492
+ try {
493
+ const response = await openai.responses.create({
494
+ model: modelId,
495
+ input: fullInput,
496
+ tools: [{ type: "web_search" }],
497
+ stream: true
498
+ });
499
+ return new ReadableStream({
500
+ async start(controller) {
501
+ try {
502
+ for await (const event of response) {
503
+ if (event.type === "response.output_text.delta") {
504
+ const text = event.delta;
505
+ if (text) {
506
+ controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ text })}
507
+
508
+ `));
509
+ }
510
+ }
511
+ }
512
+ controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
513
+ controller.close();
514
+ } catch (streamError) {
515
+ const errorMessage = streamError instanceof Error ? streamError.message : "Stream error";
516
+ console.error("[OpenAI Responses Stream Error]", streamError);
517
+ controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ error: errorMessage })}
518
+
519
+ `));
520
+ controller.close();
521
+ }
522
+ }
523
+ });
524
+ } catch (error) {
525
+ const errorMessage = error instanceof Error ? error.message : "OpenAI Responses API error";
526
+ console.error("[OpenAI Responses API Error]", error);
527
+ throw new Error(errorMessage);
528
+ }
231
529
  }
232
530
  var import_sdk, import_openai;
233
531
  var init_provider = __esm({
@@ -240,9 +538,18 @@ var init_provider = __esm({
240
538
  });
241
539
 
242
540
  // src/ai/builders.ts
541
+ var builders_exports = {};
542
+ __export(builders_exports, {
543
+ buildAutoDraftPrompt: () => buildAutoDraftPrompt,
544
+ buildChatPrompt: () => buildChatPrompt,
545
+ buildExpandPlanPrompt: () => buildExpandPlanPrompt,
546
+ buildGeneratePrompt: () => buildGeneratePrompt,
547
+ buildPlanPrompt: () => buildPlanPrompt,
548
+ buildRewritePrompt: () => buildRewritePrompt
549
+ });
243
550
  function buildGeneratePrompt(options) {
244
551
  const template = options.template || DEFAULT_GENERATE_TEMPLATE;
245
- return template.replace("{{RULES}}", options.rules || "").replace("{{WORD_COUNT}}", String(options.wordCount || 800));
552
+ return template.replace("{{RULES}}", options.rules || "").replace("{{WORD_COUNT}}", String(options.wordCount || 800)).replace("{{STYLE_EXAMPLES}}", options.styleExamples || "");
246
553
  }
247
554
  function buildChatPrompt(options) {
248
555
  const template = options.template || DEFAULT_CHAT_TEMPLATE;
@@ -257,12 +564,25 @@ Content:
257
564
  ${options.essayContext.markdown}
258
565
  `;
259
566
  }
260
- return template.replace("{{CHAT_RULES}}", options.chatRules || "").replace("{{ESSAY_CONTEXT}}", essaySection);
567
+ return template.replace("{{CHAT_RULES}}", options.chatRules || "").replace("{{RULES}}", options.rules || "").replace("{{ESSAY_CONTEXT}}", essaySection).replace("{{STYLE_EXAMPLES}}", options.styleExamples || "");
261
568
  }
262
569
  function buildExpandPlanPrompt(options) {
263
570
  const template = options.template || DEFAULT_EXPAND_PLAN_TEMPLATE;
264
571
  return template.replace("{{RULES}}", options.rules || "").replace("{{STYLE_EXAMPLES}}", options.styleExamples || "").replace("{{PLAN}}", options.plan);
265
572
  }
573
+ function buildPlanPrompt(options) {
574
+ const template = options.template || DEFAULT_PLAN_TEMPLATE;
575
+ const rules = options.planRules || DEFAULT_PLAN_RULES;
576
+ return template.replace("{{PLAN_RULES}}", rules).replace("{{STYLE_EXAMPLES}}", options.styleExamples || "");
577
+ }
578
+ function buildRewritePrompt(options) {
579
+ const template = options.template || DEFAULT_REWRITE_TEMPLATE;
580
+ return template.replace("{{REWRITE_RULES}}", options.rewriteRules || "").replace("{{RULES}}", options.rules || "").replace("{{STYLE_EXAMPLES}}", options.styleExamples || "");
581
+ }
582
+ function buildAutoDraftPrompt(options) {
583
+ const template = options.template || DEFAULT_AUTO_DRAFT_TEMPLATE;
584
+ return template.replace("{{AUTO_DRAFT_RULES}}", options.autoDraftRules || "").replace("{{RULES}}", options.rules || "").replace("{{AUTO_DRAFT_WORD_COUNT}}", String(options.wordCount || 800)).replace("{{STYLE_EXAMPLES}}", options.styleExamples || "").replace("{{TOPIC_NAME}}", options.topicName || "").replace("{{ARTICLE_TITLE}}", options.articleTitle || "").replace("{{ARTICLE_SUMMARY}}", options.articleSummary || "").replace("{{ARTICLE_URL}}", options.articleUrl || "");
585
+ }
266
586
  var init_builders = __esm({
267
587
  "src/ai/builders.ts"() {
268
588
  "use strict";
@@ -270,6 +590,228 @@ var init_builders = __esm({
270
590
  }
271
591
  });
272
592
 
593
+ // src/lib/url-extractor.ts
594
+ var url_extractor_exports = {};
595
+ __export(url_extractor_exports, {
596
+ buildUrlContext: () => buildUrlContext,
597
+ extractAndFetchUrls: () => extractAndFetchUrls,
598
+ extractUrls: () => extractUrls,
599
+ fetchUrlContent: () => fetchUrlContent
600
+ });
601
+ function isServerlessEnvironment() {
602
+ return !!(process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.VERCEL || process.env.NETLIFY || process.env.AWS_EXECUTION_ENV);
603
+ }
604
+ function extractUrls(text) {
605
+ const urls = [];
606
+ const withProtocol = text.match(URL_WITH_PROTOCOL);
607
+ if (withProtocol) urls.push(...withProtocol);
608
+ const wwwUrls = text.match(URL_WITHOUT_PROTOCOL);
609
+ if (wwwUrls) {
610
+ for (const url of wwwUrls) {
611
+ const normalized = `https://${url}`;
612
+ if (!urls.some((u) => u.includes(url))) {
613
+ urls.push(normalized);
614
+ }
615
+ }
616
+ }
617
+ const bareUrls = text.match(DOMAIN_ONLY);
618
+ if (bareUrls) {
619
+ for (const url of bareUrls) {
620
+ const normalized = `https://${url}`;
621
+ if (!urls.some((u) => u.includes(url.split("/")[0]))) {
622
+ urls.push(normalized);
623
+ }
624
+ }
625
+ }
626
+ return [...new Set(urls)];
627
+ }
628
+ function extractTextFromHtml(html, url) {
629
+ const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
630
+ const title = titleMatch ? titleMatch[1].trim() : void 0;
631
+ let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, "").replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, "").replace(/<header[^>]*>[\s\S]*?<\/header>/gi, "").replace(/<aside[^>]*>[\s\S]*?<\/aside>/gi, "").replace(/<!--[\s\S]*?-->/g, "").replace(/<(p|div|br|h[1-6]|li|tr)[^>]*>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&[a-z]+;/gi, " ").replace(/\s+/g, " ").replace(/\n\s+/g, "\n").replace(/\n+/g, "\n").trim();
632
+ if (text.length > 4e3) {
633
+ text = text.slice(0, 4e3) + "\n\n[Content truncated...]";
634
+ }
635
+ if (text.length < 50) {
636
+ return { url, content: "", error: "Could not extract meaningful content" };
637
+ }
638
+ return { url, title, content: text };
639
+ }
640
+ async function parseWithReadability(html, url) {
641
+ try {
642
+ const { JSDOM } = await import("jsdom");
643
+ const { Readability } = await import("@mozilla/readability");
644
+ const doc = new JSDOM(html, {
645
+ url,
646
+ resources: void 0,
647
+ // Don't load ANY external resources (stylesheets, etc.)
648
+ runScripts: void 0
649
+ // Don't run any scripts
650
+ });
651
+ const reader = new Readability(doc.window.document);
652
+ const article = reader.parse();
653
+ if (!article || !article.textContent) {
654
+ console.log("[Readability] No article content, falling back to simple extraction");
655
+ return extractTextFromHtml(html, url);
656
+ }
657
+ let content = article.textContent.trim();
658
+ if (content.length > 4e3) {
659
+ content = content.slice(0, 4e3) + "\n\n[Content truncated...]";
660
+ }
661
+ return {
662
+ url,
663
+ title: article.title || void 0,
664
+ content
665
+ };
666
+ } catch (error) {
667
+ console.error("[JSDOM] Failed, using simple extraction:", error instanceof Error ? error.message : error);
668
+ return extractTextFromHtml(html, url);
669
+ }
670
+ }
671
+ async function fetchWithPuppeteer(url) {
672
+ let browser = null;
673
+ const isServerless = isServerlessEnvironment();
674
+ try {
675
+ console.log(`[Puppeteer] Launching browser for: ${url} (serverless: ${isServerless})`);
676
+ if (isServerless) {
677
+ const chromium = await import("@sparticuz/chromium");
678
+ const puppeteerCore = await import("puppeteer-core");
679
+ const executablePath = await chromium.default.executablePath();
680
+ browser = await puppeteerCore.default.launch({
681
+ args: chromium.default.args,
682
+ defaultViewport: chromium.default.defaultViewport,
683
+ executablePath,
684
+ headless: chromium.default.headless
685
+ });
686
+ } else {
687
+ try {
688
+ const puppeteer = await import("puppeteer");
689
+ browser = await puppeteer.default.launch({
690
+ headless: true,
691
+ args: [
692
+ "--no-sandbox",
693
+ "--disable-setuid-sandbox",
694
+ "--disable-dev-shm-usage",
695
+ "--disable-accelerated-2d-canvas",
696
+ "--disable-gpu"
697
+ ]
698
+ });
699
+ } catch (puppeteerImportError) {
700
+ console.error("[Puppeteer] Import/launch failed:", puppeteerImportError);
701
+ return { url, content: "", error: "Puppeteer unavailable - falling back to simple fetch" };
702
+ }
703
+ }
704
+ const page = await browser.newPage();
705
+ await page.setViewport({ width: 1920, height: 1080 });
706
+ await page.setUserAgent(
707
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
708
+ );
709
+ await page.setRequestInterception(true);
710
+ page.on("request", (req) => {
711
+ const resourceType = req.resourceType();
712
+ if (["image", "stylesheet", "font", "media"].includes(resourceType)) {
713
+ req.abort();
714
+ } else {
715
+ req.continue();
716
+ }
717
+ });
718
+ await page.goto(url, {
719
+ waitUntil: "networkidle2",
720
+ timeout: PUPPETEER_TIMEOUT
721
+ });
722
+ await new Promise((resolve) => setTimeout(resolve, CONTENT_WAIT_TIME));
723
+ const html = await page.content();
724
+ await browser.close();
725
+ browser = null;
726
+ console.log("[Puppeteer] Got HTML, parsing with Readability...");
727
+ return await parseWithReadability(html, url);
728
+ } catch (error) {
729
+ const errorMessage = error instanceof Error ? error.message : "Puppeteer error";
730
+ console.error("[Puppeteer] Failed:", errorMessage);
731
+ return { url, content: "", error: `Puppeteer: ${errorMessage}` };
732
+ } finally {
733
+ if (browser) {
734
+ try {
735
+ await browser.close();
736
+ } catch {
737
+ }
738
+ }
739
+ }
740
+ }
741
+ async function fetchWithSimpleRequest(url) {
742
+ try {
743
+ console.log("[SimpleFetch] Fetching:", url);
744
+ const res = await fetch(url, {
745
+ headers: {
746
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
747
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
748
+ "Accept-Language": "en-US,en;q=0.9"
749
+ }
750
+ });
751
+ if (!res.ok) {
752
+ return { url, content: "", error: `HTTP ${res.status}` };
753
+ }
754
+ const html = await res.text();
755
+ return parseWithReadability(html, url);
756
+ } catch (error) {
757
+ return {
758
+ url,
759
+ content: "",
760
+ error: error instanceof Error ? error.message : "Failed to fetch"
761
+ };
762
+ }
763
+ }
764
+ async function fetchUrlContent(url) {
765
+ console.log("[URL Extractor] Fetching content from:", url);
766
+ const puppeteerResult = await fetchWithPuppeteer(url);
767
+ if (!puppeteerResult.error && puppeteerResult.content && puppeteerResult.content.length > 100) {
768
+ console.log("[URL Extractor] Puppeteer succeeded, got", puppeteerResult.content.length, "chars");
769
+ return puppeteerResult;
770
+ }
771
+ console.log("[URL Extractor] Puppeteer failed or got minimal content, trying simple fetch...");
772
+ const simpleResult = await fetchWithSimpleRequest(url);
773
+ if (simpleResult.content && simpleResult.content.length > (puppeteerResult.content?.length || 0)) {
774
+ console.log("[URL Extractor] Simple fetch got more content:", simpleResult.content.length, "chars");
775
+ return simpleResult;
776
+ }
777
+ if (puppeteerResult.content && puppeteerResult.content.length > 0) {
778
+ return puppeteerResult;
779
+ }
780
+ return simpleResult.error ? simpleResult : puppeteerResult;
781
+ }
782
+ async function extractAndFetchUrls(text) {
783
+ const urls = extractUrls(text);
784
+ if (urls.length === 0) return [];
785
+ const toFetch = urls.slice(0, 3);
786
+ const results = await Promise.all(toFetch.map((url) => fetchUrlContent(url)));
787
+ return results;
788
+ }
789
+ function buildUrlContext(fetched) {
790
+ const successful = fetched.filter((f) => !f.error && f.content);
791
+ if (successful.length === 0) return "";
792
+ return `
793
+ <referenced_urls>
794
+ ${successful.map(
795
+ (f) => `<url src="${f.url}"${f.title ? ` title="${f.title}"` : ""}>
796
+ ${f.content}
797
+ </url>`
798
+ ).join("\n\n")}
799
+ </referenced_urls>
800
+
801
+ Use the content from these URLs when relevant to the conversation.`;
802
+ }
803
+ var URL_WITH_PROTOCOL, URL_WITHOUT_PROTOCOL, DOMAIN_ONLY, PUPPETEER_TIMEOUT, CONTENT_WAIT_TIME;
804
+ var init_url_extractor = __esm({
805
+ "src/lib/url-extractor.ts"() {
806
+ "use strict";
807
+ URL_WITH_PROTOCOL = /https?:\/\/[^\s<>\[\]()]+(?:\([^\s<>\[\]()]*\))?[^\s<>\[\]().,;:!?"']*(?<![.,;:!?"'])/gi;
808
+ URL_WITHOUT_PROTOCOL = /(?:www\.)[a-zA-Z0-9][-a-zA-Z0-9]*(?:\.[a-zA-Z]{2,})+(?:\/[^\s<>\[\]()]*)?/gi;
809
+ DOMAIN_ONLY = /(?<![/@])(?:[a-zA-Z0-9][-a-zA-Z0-9]*\.)+(?:com|org|net|edu|gov|io|co|app|dev|news|info)(?:\/[^\s<>\[\]()]*)?(?![a-zA-Z])/gi;
810
+ PUPPETEER_TIMEOUT = 15e3;
811
+ CONTENT_WAIT_TIME = 2e3;
812
+ }
813
+ });
814
+
273
815
  // src/ai/generate.ts
274
816
  var generate_exports = {};
275
817
  __export(generate_exports, {
@@ -280,17 +822,41 @@ async function generateStream(options) {
280
822
  const systemPrompt = buildGeneratePrompt({
281
823
  rules: options.rules,
282
824
  template: options.template,
283
- wordCount: options.wordCount
825
+ wordCount: options.wordCount,
826
+ styleExamples: options.styleExamples
284
827
  });
828
+ let enrichedPrompt = options.prompt;
829
+ if (options.useWebSearch) {
830
+ try {
831
+ const fetched = await extractAndFetchUrls(options.prompt);
832
+ const successful = fetched.filter((f) => !f.error && f.content);
833
+ if (successful.length > 0) {
834
+ enrichedPrompt = `${options.prompt}
835
+
836
+ <source_material>
837
+ ${successful.map(
838
+ (f) => `Source: ${f.url}${f.title ? ` (${f.title})` : ""}
839
+ ${f.content}`
840
+ ).join("\n\n---\n\n")}
841
+ </source_material>
842
+
843
+ Use the source material above as reference for the essay.`;
844
+ }
845
+ } catch (err) {
846
+ console.warn("URL extraction failed:", err);
847
+ }
848
+ }
285
849
  return createStream({
286
850
  model: options.model,
287
851
  messages: [
288
852
  { role: "system", content: systemPrompt },
289
- { role: "user", content: options.prompt }
853
+ { role: "user", content: enrichedPrompt }
290
854
  ],
291
855
  anthropicKey: options.anthropicKey,
292
856
  openaiKey: options.openaiKey,
293
- maxTokens: 8192
857
+ maxTokens: options.useThinking ? 16e3 : 8192,
858
+ useWebSearch: options.useWebSearch,
859
+ useThinking: options.useThinking
294
860
  });
295
861
  }
296
862
  async function expandPlanStream(options) {
@@ -316,6 +882,7 @@ var init_generate = __esm({
316
882
  "use strict";
317
883
  init_provider();
318
884
  init_builders();
885
+ init_url_extractor();
319
886
  }
320
887
  });
321
888
 
@@ -325,30 +892,107 @@ __export(chat_exports, {
325
892
  chatStream: () => chatStream
326
893
  });
327
894
  async function chatStream(options) {
328
- const systemPrompt = buildChatPrompt({
895
+ const systemPrompt = options.mode === "plan" ? buildPlanPrompt({
896
+ planRules: options.planRules,
897
+ template: options.planTemplate,
898
+ styleExamples: options.styleExamples
899
+ }) : buildChatPrompt({
329
900
  chatRules: options.chatRules,
901
+ rules: options.rules,
330
902
  template: options.template,
331
- essayContext: options.essayContext
903
+ essayContext: options.essayContext,
904
+ styleExamples: options.styleExamples
332
905
  });
906
+ let urlContext = "";
907
+ let urlExtractionStatus = "";
908
+ if (options.useWebSearch) {
909
+ const lastUserMsg = [...options.messages].reverse().find((m) => m.role === "user");
910
+ if (lastUserMsg) {
911
+ try {
912
+ const { extractUrls: extractUrls2 } = await Promise.resolve().then(() => (init_url_extractor(), url_extractor_exports));
913
+ const detectedUrls = extractUrls2(lastUserMsg.content);
914
+ if (detectedUrls.length > 0) {
915
+ console.log("[URL Extraction] Detected URLs:", detectedUrls);
916
+ const fetched = await extractAndFetchUrls(lastUserMsg.content);
917
+ if (fetched.length > 0) {
918
+ const successful = fetched.filter((f) => !f.error && f.content);
919
+ const failed = fetched.filter((f) => f.error || !f.content);
920
+ if (successful.length > 0) {
921
+ urlContext = buildUrlContext(fetched);
922
+ console.log("[URL Extraction] Successfully fetched:", successful.map((f) => f.url));
923
+ }
924
+ if (failed.length > 0) {
925
+ console.warn("[URL Extraction] Failed to fetch:", failed.map((f) => ({ url: f.url, error: f.error })));
926
+ urlExtractionStatus = `
927
+
928
+ <url_extraction_status>
929
+ Attempted to fetch ${detectedUrls.length} URL(s). ${successful.length} succeeded, ${failed.length} failed.
930
+ ${failed.map((f) => `- ${f.url}: ${f.error || "Empty content"}`).join("\n")}
931
+ </url_extraction_status>`;
932
+ }
933
+ }
934
+ }
935
+ } catch (err) {
936
+ console.error("[URL Extraction] Error:", err);
937
+ urlExtractionStatus = `
938
+
939
+ <url_extraction_status>
940
+ URL extraction encountered an error: ${err instanceof Error ? err.message : "Unknown error"}
941
+ </url_extraction_status>`;
942
+ }
943
+ }
944
+ }
333
945
  let modeInstructions = "";
334
946
  if (options.mode === "agent") {
335
- modeInstructions = `
336
- You can directly edit the essay using these commands:
337
- - To replace text: :::edit replace_section "old text" "new text" :::
338
- - To replace all: :::edit replace_all "title" "subtitle" "full markdown" :::
339
- - To insert: :::edit insert "position" "match_text" "new_text" ::: (position: before, after, start, end)
340
- - To delete: :::edit delete "text to remove" :::
341
- `;
947
+ modeInstructions = "\n\n" + (options.agentTemplate || DEFAULT_AGENT_TEMPLATE);
342
948
  }
949
+ let webSearchContext = "";
950
+ if (options.useWebSearch) {
951
+ webSearchContext = `
952
+
953
+ <search_capability>
954
+ Web search is ENABLED for this conversation. You have access to current, real-time information from the web.
955
+ - For questions about recent events, news, or current information: search results will be provided automatically
956
+ - Cite sources when referencing specific facts from search results
957
+ - If search results are included in <web_search_results> tags, use that information to provide accurate, up-to-date answers
958
+ </search_capability>`;
959
+ }
960
+ let thinkingInstructions = "";
961
+ if (options.useThinking) {
962
+ thinkingInstructions = "\n\nTake time to think through complex questions step by step. Consider multiple angles before providing your final answer.";
963
+ }
964
+ let urlCapabilityInstructions = "";
965
+ if (options.useWebSearch) {
966
+ const hasUrlContent = urlContext.length > 0;
967
+ const hasFailedUrls = urlExtractionStatus.length > 0;
968
+ urlCapabilityInstructions = `
969
+
970
+ <CRITICAL_CAPABILITY>
971
+ **IMPORTANT: URL READING IS ENABLED**
972
+
973
+ You have a WORKING URL reader in this application. This overrides your default training.
974
+ - DO NOT say "I cannot access URLs" or "I cannot browse the web" - you CAN and DO have this capability
975
+ - When users share URLs, the content is AUTOMATICALLY FETCHED by the system
976
+ - Look for <referenced_urls> tags below - that contains the fetched page content
977
+ - ${hasUrlContent ? "URL CONTENT WAS SUCCESSFULLY FETCHED - see <referenced_urls> below" : hasFailedUrls ? "URL fetch was ATTEMPTED but FAILED - see <url_extraction_status> below for details" : "No URLs detected in the current message"}
978
+
979
+ If you see fetched content, use it to answer the user's question. Quote specific passages when relevant.
980
+ If the fetch failed, explain what happened using the error details provided.
981
+ </CRITICAL_CAPABILITY>`;
982
+ }
983
+ const filteredMessages = options.messages.filter((m) => m.content && m.content.trim().length > 0);
343
984
  return createStream({
344
985
  model: options.model,
345
986
  messages: [
346
- { role: "system", content: systemPrompt + modeInstructions },
347
- ...options.messages
987
+ { role: "system", content: systemPrompt + modeInstructions + webSearchContext + thinkingInstructions + urlCapabilityInstructions + urlContext + urlExtractionStatus },
988
+ ...filteredMessages
348
989
  ],
349
990
  anthropicKey: options.anthropicKey,
350
991
  openaiKey: options.openaiKey,
351
- maxTokens: 4096
992
+ maxTokens: options.useThinking ? 16e3 : 4096,
993
+ // Allow more tokens for thinking mode
994
+ useThinking: options.useThinking,
995
+ useWebSearch: options.useWebSearch
352
996
  });
353
997
  }
354
998
  var init_chat = __esm({
@@ -356,6 +1000,8 @@ var init_chat = __esm({
356
1000
  "use strict";
357
1001
  init_provider();
358
1002
  init_builders();
1003
+ init_url_extractor();
1004
+ init_prompts();
359
1005
  }
360
1006
  });
361
1007
 
@@ -373,8 +1019,12 @@ __export(src_exports, {
373
1019
  DEFAULT_REWRITE_TEMPLATE: () => DEFAULT_REWRITE_TEMPLATE,
374
1020
  addCommentMark: () => addCommentMark,
375
1021
  applyCommentMarks: () => applyCommentMarks,
1022
+ buildAutoDraftPrompt: () => buildAutoDraftPrompt,
376
1023
  buildChatPrompt: () => buildChatPrompt,
1024
+ buildExpandPlanPrompt: () => buildExpandPlanPrompt,
377
1025
  buildGeneratePrompt: () => buildGeneratePrompt,
1026
+ buildPlanPrompt: () => buildPlanPrompt,
1027
+ buildRewritePrompt: () => buildRewritePrompt,
378
1028
  canDeleteComment: () => canDeleteComment,
379
1029
  canEditComment: () => canEditComment,
380
1030
  createAPIHandler: () => createAPIHandler,
@@ -1331,15 +1981,21 @@ async function handleAIAPI(req, cms, session, path) {
1331
1981
  if (authError) return authError;
1332
1982
  if (method === "GET" && path === "/ai/settings") {
1333
1983
  const settings = await cms.aiSettings.get();
1984
+ const hasAnthropicEnvKey = !!(cms.config.ai?.anthropicKey || process.env.ANTHROPIC_API_KEY);
1985
+ const hasOpenaiEnvKey = !!(cms.config.ai?.openaiKey || process.env.OPENAI_API_KEY);
1334
1986
  return jsonResponse2({
1335
1987
  data: {
1336
1988
  ...settings,
1989
+ // Don't expose actual env keys, just indicate they exist
1990
+ hasAnthropicEnvKey,
1991
+ hasOpenaiEnvKey,
1337
1992
  defaultGenerateTemplate: DEFAULT_GENERATE_TEMPLATE,
1338
1993
  defaultChatTemplate: DEFAULT_CHAT_TEMPLATE,
1339
1994
  defaultRewriteTemplate: DEFAULT_REWRITE_TEMPLATE,
1340
1995
  defaultAutoDraftTemplate: DEFAULT_AUTO_DRAFT_TEMPLATE,
1341
1996
  defaultPlanTemplate: DEFAULT_PLAN_TEMPLATE,
1342
1997
  defaultExpandPlanTemplate: DEFAULT_EXPAND_PLAN_TEMPLATE,
1998
+ defaultAgentTemplate: DEFAULT_AGENT_TEMPLATE,
1343
1999
  defaultPlanRules: DEFAULT_PLAN_RULES
1344
2000
  }
1345
2001
  });
@@ -1353,11 +2009,36 @@ async function handleAIAPI(req, cms, session, path) {
1353
2009
  }
1354
2010
  if (method === "POST" && path === "/ai/generate") {
1355
2011
  const body = await req.json();
1356
- const { prompt, model, wordCount, mode, plan, styleExamples } = body;
2012
+ const { prompt, model, wordCount, mode, plan, styleExamples: clientStyleExamples, useWebSearch, useThinking } = body;
1357
2013
  const settings = await cms.aiSettings.get();
1358
2014
  try {
1359
2015
  let stream;
2016
+ const anthropicKey = cms.config.ai?.anthropicKey || settings.anthropicKey;
2017
+ const openaiKey = cms.config.ai?.openaiKey || settings.openaiKey;
1360
2018
  if (mode === "expand_plan" && plan) {
2019
+ let styleExamples = clientStyleExamples || "";
2020
+ if (!styleExamples) {
2021
+ try {
2022
+ const publishedPosts = await cms.posts.findPublished();
2023
+ const MAX_STYLE_EXAMPLES = 5;
2024
+ const MAX_WORDS_PER_EXAMPLE = 500;
2025
+ if (publishedPosts.length > 0) {
2026
+ const examples = publishedPosts.slice(0, MAX_STYLE_EXAMPLES).map((post) => {
2027
+ const words = post.markdown.split(/\s+/);
2028
+ const truncatedContent = words.length > MAX_WORDS_PER_EXAMPLE ? words.slice(0, MAX_WORDS_PER_EXAMPLE).join(" ") + "..." : post.markdown;
2029
+ return `## ${post.title}
2030
+ ${post.subtitle ? `*${post.subtitle}*
2031
+ ` : ""}
2032
+ ${truncatedContent}`;
2033
+ }).join("\n\n---\n\n");
2034
+ styleExamples = `The following are examples of the author's published work. Use these to match their voice, tone, and writing style:
2035
+
2036
+ ${examples}`;
2037
+ }
2038
+ } catch (err) {
2039
+ console.error("[AI Generate] Failed to fetch published essays:", err);
2040
+ }
2041
+ }
1361
2042
  const { expandPlanStream: expandPlanStream2 } = await Promise.resolve().then(() => (init_generate(), generate_exports));
1362
2043
  stream = await expandPlanStream2({
1363
2044
  plan,
@@ -1365,10 +2046,33 @@ async function handleAIAPI(req, cms, session, path) {
1365
2046
  rules: settings.rules,
1366
2047
  template: settings.expandPlanTemplate,
1367
2048
  styleExamples,
1368
- anthropicKey: cms.config.ai?.anthropicKey,
1369
- openaiKey: cms.config.ai?.openaiKey
2049
+ anthropicKey,
2050
+ openaiKey
1370
2051
  });
1371
2052
  } else {
2053
+ let styleExamples = clientStyleExamples || "";
2054
+ if (!styleExamples) {
2055
+ try {
2056
+ const publishedPosts = await cms.posts.findPublished();
2057
+ const MAX_STYLE_EXAMPLES = 5;
2058
+ const MAX_WORDS_PER_EXAMPLE = 500;
2059
+ if (publishedPosts.length > 0) {
2060
+ const examples = publishedPosts.slice(0, MAX_STYLE_EXAMPLES).map((post) => {
2061
+ const words = post.markdown.split(/\s+/);
2062
+ const truncatedContent = words.length > MAX_WORDS_PER_EXAMPLE ? words.slice(0, MAX_WORDS_PER_EXAMPLE).join(" ") + "..." : post.markdown;
2063
+ return `## ${post.title}
2064
+ ${post.subtitle ? `*${post.subtitle}*
2065
+ ` : ""}
2066
+ ${truncatedContent}`;
2067
+ }).join("\n\n---\n\n");
2068
+ styleExamples = `The following are examples of the author's published work. Use these to match their voice, tone, and writing style:
2069
+
2070
+ ${examples}`;
2071
+ }
2072
+ } catch (err) {
2073
+ console.error("[AI Generate] Failed to fetch published essays:", err);
2074
+ }
2075
+ }
1372
2076
  const { generateStream: generateStream2 } = await Promise.resolve().then(() => (init_generate(), generate_exports));
1373
2077
  stream = await generateStream2({
1374
2078
  prompt,
@@ -1376,8 +2080,11 @@ async function handleAIAPI(req, cms, session, path) {
1376
2080
  wordCount,
1377
2081
  rules: settings.rules,
1378
2082
  template: settings.generateTemplate,
1379
- anthropicKey: cms.config.ai?.anthropicKey,
1380
- openaiKey: cms.config.ai?.openaiKey
2083
+ styleExamples,
2084
+ anthropicKey,
2085
+ openaiKey,
2086
+ useWebSearch,
2087
+ useThinking
1381
2088
  });
1382
2089
  }
1383
2090
  return new Response(stream, {
@@ -1388,6 +2095,7 @@ async function handleAIAPI(req, cms, session, path) {
1388
2095
  }
1389
2096
  });
1390
2097
  } catch (error) {
2098
+ console.error("[AI Generate Error]", error);
1391
2099
  return jsonResponse2({
1392
2100
  error: error instanceof Error ? error.message : "Generation failed"
1393
2101
  }, 500);
@@ -1395,8 +2103,33 @@ async function handleAIAPI(req, cms, session, path) {
1395
2103
  }
1396
2104
  if (method === "POST" && path === "/ai/chat") {
1397
2105
  const body = await req.json();
1398
- const { messages, model, essayContext, mode } = body;
2106
+ const { messages, model, essayContext, mode, useWebSearch, useThinking } = body;
1399
2107
  const settings = await cms.aiSettings.get();
2108
+ const anthropicKey = cms.config.ai?.anthropicKey || settings.anthropicKey;
2109
+ const openaiKey = cms.config.ai?.openaiKey || settings.openaiKey;
2110
+ let styleExamples = "";
2111
+ try {
2112
+ const publishedPosts = await cms.posts.findPublished();
2113
+ const MAX_STYLE_EXAMPLES = 5;
2114
+ const MAX_WORDS_PER_EXAMPLE = 500;
2115
+ if (publishedPosts.length > 0) {
2116
+ const examples = publishedPosts.slice(0, MAX_STYLE_EXAMPLES).map((post) => {
2117
+ const words = post.markdown.split(/\s+/);
2118
+ const truncatedContent = words.length > MAX_WORDS_PER_EXAMPLE ? words.slice(0, MAX_WORDS_PER_EXAMPLE).join(" ") + "..." : post.markdown;
2119
+ return `## ${post.title}
2120
+ ${post.subtitle ? `*${post.subtitle}*
2121
+ ` : ""}
2122
+ ${truncatedContent}`;
2123
+ }).join("\n\n---\n\n");
2124
+ styleExamples = `<published_essays>
2125
+ The following are examples of the author's published work. Use these to match their voice, tone, and writing style:
2126
+
2127
+ ${examples}
2128
+ </published_essays>`;
2129
+ }
2130
+ } catch (err) {
2131
+ console.error("[AI Chat] Failed to fetch published essays:", err);
2132
+ }
1400
2133
  const { chatStream: chatStream2 } = await Promise.resolve().then(() => (init_chat(), chat_exports));
1401
2134
  try {
1402
2135
  const stream = await chatStream2({
@@ -1405,9 +2138,18 @@ async function handleAIAPI(req, cms, session, path) {
1405
2138
  essayContext,
1406
2139
  mode,
1407
2140
  chatRules: settings.chatRules,
2141
+ rules: settings.rules,
1408
2142
  template: settings.chatTemplate,
1409
- anthropicKey: cms.config.ai?.anthropicKey,
1410
- openaiKey: cms.config.ai?.openaiKey
2143
+ // Plan mode specific settings
2144
+ planTemplate: settings.planTemplate,
2145
+ planRules: settings.planRules,
2146
+ // Agent mode specific settings
2147
+ agentTemplate: settings.agentTemplate,
2148
+ styleExamples,
2149
+ anthropicKey,
2150
+ openaiKey,
2151
+ useWebSearch,
2152
+ useThinking
1411
2153
  });
1412
2154
  return new Response(stream, {
1413
2155
  headers: {
@@ -1417,11 +2159,89 @@ async function handleAIAPI(req, cms, session, path) {
1417
2159
  }
1418
2160
  });
1419
2161
  } catch (error) {
2162
+ console.error("[AI Chat Error]", error);
1420
2163
  return jsonResponse2({
1421
2164
  error: error instanceof Error ? error.message : "Chat failed"
1422
2165
  }, 500);
1423
2166
  }
1424
2167
  }
2168
+ if (method === "POST" && path === "/ai/rewrite") {
2169
+ const body = await req.json();
2170
+ const { text } = body;
2171
+ if (!text || typeof text !== "string") {
2172
+ return jsonResponse2({ error: "Text is required" }, 400);
2173
+ }
2174
+ const settings = await cms.aiSettings.get();
2175
+ const anthropicKey = cms.config.ai?.anthropicKey || settings.anthropicKey;
2176
+ const openaiKey = cms.config.ai?.openaiKey || settings.openaiKey;
2177
+ let styleExamples = "";
2178
+ try {
2179
+ const publishedPosts = await cms.posts.findPublished();
2180
+ const MAX_STYLE_EXAMPLES = 3;
2181
+ const MAX_WORDS_PER_EXAMPLE = 300;
2182
+ if (publishedPosts.length > 0) {
2183
+ const examples = publishedPosts.slice(0, MAX_STYLE_EXAMPLES).map((post) => {
2184
+ const words = post.markdown.split(/\s+/);
2185
+ const truncatedContent = words.length > MAX_WORDS_PER_EXAMPLE ? words.slice(0, MAX_WORDS_PER_EXAMPLE).join(" ") + "..." : post.markdown;
2186
+ return `## ${post.title}
2187
+ ${truncatedContent}`;
2188
+ }).join("\n\n---\n\n");
2189
+ styleExamples = examples;
2190
+ }
2191
+ } catch (err) {
2192
+ console.error("[AI Rewrite] Failed to fetch published essays:", err);
2193
+ }
2194
+ const { buildRewritePrompt: buildRewritePrompt2 } = await Promise.resolve().then(() => (init_builders(), builders_exports));
2195
+ const { createStream: createStream2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
2196
+ try {
2197
+ const systemPrompt = buildRewritePrompt2({
2198
+ rewriteRules: settings.rewriteRules,
2199
+ rules: settings.rules,
2200
+ template: settings.rewriteTemplate,
2201
+ styleExamples
2202
+ });
2203
+ const stream = await createStream2({
2204
+ model: settings.defaultModel,
2205
+ messages: [
2206
+ { role: "system", content: systemPrompt },
2207
+ { role: "user", content: `Rewrite the following text, preserving meaning but improving clarity and style:
2208
+
2209
+ ${text}` }
2210
+ ],
2211
+ anthropicKey,
2212
+ openaiKey,
2213
+ maxTokens: 2048
2214
+ });
2215
+ const reader = stream.getReader();
2216
+ const decoder = new TextDecoder();
2217
+ let rewrittenText = "";
2218
+ while (true) {
2219
+ const { done, value } = await reader.read();
2220
+ if (done) break;
2221
+ const chunk = decoder.decode(value, { stream: true });
2222
+ const lines = chunk.split("\n");
2223
+ for (const line of lines) {
2224
+ if (line.startsWith("data: ")) {
2225
+ const data = line.slice(6);
2226
+ if (data === "[DONE]") continue;
2227
+ try {
2228
+ const parsed = JSON.parse(data);
2229
+ if (parsed.text) {
2230
+ rewrittenText += parsed.text;
2231
+ }
2232
+ } catch {
2233
+ }
2234
+ }
2235
+ }
2236
+ }
2237
+ return jsonResponse2({ text: rewrittenText.trim() });
2238
+ } catch (error) {
2239
+ console.error("[AI Rewrite Error]", error);
2240
+ return jsonResponse2({
2241
+ error: error instanceof Error ? error.message : "Rewrite failed"
2242
+ }, 500);
2243
+ }
2244
+ }
1425
2245
  return jsonResponse2({ error: "Not found" }, 404);
1426
2246
  }
1427
2247
 
@@ -1570,7 +2390,8 @@ async function handleSettingsAPI(req, cms, session, path) {
1570
2390
  });
1571
2391
  return jsonResponse2({
1572
2392
  data: {
1573
- autoDraftEnabled: integrationSettings?.autoDraftEnabled ?? false
2393
+ autoDraftEnabled: integrationSettings?.autoDraftEnabled ?? false,
2394
+ postUrlPattern: integrationSettings?.postUrlPattern ?? "/e/{slug}"
1574
2395
  }
1575
2396
  });
1576
2397
  }
@@ -1578,11 +2399,18 @@ async function handleSettingsAPI(req, cms, session, path) {
1578
2399
  const adminError = requireAdmin(cms, session);
1579
2400
  if (adminError) return adminError;
1580
2401
  const body = await req.json();
2402
+ const updateData = {};
1581
2403
  if (typeof body.autoDraftEnabled === "boolean") {
2404
+ updateData.autoDraftEnabled = body.autoDraftEnabled;
2405
+ }
2406
+ if (typeof body.postUrlPattern === "string") {
2407
+ updateData.postUrlPattern = body.postUrlPattern;
2408
+ }
2409
+ if (Object.keys(updateData).length > 0) {
1582
2410
  await prisma.integrationSettings.upsert({
1583
2411
  where: { id: "default" },
1584
- create: { id: "default", autoDraftEnabled: body.autoDraftEnabled },
1585
- update: { autoDraftEnabled: body.autoDraftEnabled }
2412
+ create: { id: "default", ...updateData },
2413
+ update: updateData
1586
2414
  });
1587
2415
  }
1588
2416
  const integrationSettings = await prisma.integrationSettings.findUnique({
@@ -1590,7 +2418,8 @@ async function handleSettingsAPI(req, cms, session, path) {
1590
2418
  });
1591
2419
  return jsonResponse2({
1592
2420
  data: {
1593
- autoDraftEnabled: integrationSettings?.autoDraftEnabled ?? false
2421
+ autoDraftEnabled: integrationSettings?.autoDraftEnabled ?? false,
2422
+ postUrlPattern: integrationSettings?.postUrlPattern ?? "/e/{slug}"
1594
2423
  }
1595
2424
  });
1596
2425
  }
@@ -6067,8 +6896,12 @@ function scrollToComment(editor, commentId) {
6067
6896
  DEFAULT_REWRITE_TEMPLATE,
6068
6897
  addCommentMark,
6069
6898
  applyCommentMarks,
6899
+ buildAutoDraftPrompt,
6070
6900
  buildChatPrompt,
6901
+ buildExpandPlanPrompt,
6071
6902
  buildGeneratePrompt,
6903
+ buildPlanPrompt,
6904
+ buildRewritePrompt,
6072
6905
  canDeleteComment,
6073
6906
  canEditComment,
6074
6907
  createAPIHandler,