backend-manager 5.8.2 → 5.9.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/CHANGELOG.md +35 -0
- package/CLAUDE.md +1 -1
- package/PROGRESS.md +4 -4
- package/docs/admin-post-route.md +1 -1
- package/docs/ghostii.md +89 -61
- package/docs/marketing-campaigns.md +2 -2
- package/package.json +1 -1
- package/src/manager/events/cron/daily/blog-auto-publisher.js +680 -0
- package/src/manager/libraries/content/feed-parser.js +1 -1
- package/src/manager/libraries/content/ghostii.js +3 -3
- package/src/manager/libraries/email/generators/lib/structure.js +5 -1
- package/src/manager/libraries/email/generators/newsletter.js +47 -46
- package/src/manager/routes/brand/get.js +1 -1
- package/templates/backend-manager-config.json +66 -70
- package/test/helpers/content/blog-auto-publisher.js +484 -0
- package/test/helpers/content/ghostii-feed-integration.js +33 -28
- package/test/helpers/content/ghostii-write-article.js +1 -1
- package/test/routes/test/usage.js +1 -1
- package/src/manager/events/cron/daily/ghostii-auto-publisher.js +0 -511
- package/test/helpers/content/ghostii-auto-publisher.js +0 -342
|
@@ -211,6 +211,8 @@ async function generateStructure({ sources, brand, newsletterConfig, ai, assista
|
|
|
211
211
|
function buildClassicSystemPrompt(brand, config) {
|
|
212
212
|
const tone = config?.tone || 'professional';
|
|
213
213
|
const instructions = config?.instructions || '';
|
|
214
|
+
const links = config?.links || [];
|
|
215
|
+
const keywords = config?.keywords || [];
|
|
214
216
|
const taglineLine = brand?.tagline ? `\nTagline: ${brand.tagline}` : '';
|
|
215
217
|
const descriptionLine = brand?.description ? `\nDescription: ${brand.description}` : '';
|
|
216
218
|
|
|
@@ -218,6 +220,8 @@ function buildClassicSystemPrompt(brand, config) {
|
|
|
218
220
|
`You are a newsletter writer for ${brand?.name || 'a tech company'}.${taglineLine}${descriptionLine}`,
|
|
219
221
|
instructions ? `\nBrand instructions:\n${instructions}` : '',
|
|
220
222
|
`\nTone: ${tone}`,
|
|
223
|
+
keywords.length ? `\nKeywords to weave in naturally: ${keywords.join(', ')}` : '',
|
|
224
|
+
links.length ? `\nBrand links to incorporate where relevant: ${links.join(', ')}` : '',
|
|
221
225
|
'',
|
|
222
226
|
'You will be given a set of "source articles" — these are background research, NOT publications you are writing for or about.',
|
|
223
227
|
'Treat them as raw information. Synthesize the IDEAS into original content written as if you are the original author.',
|
|
@@ -246,7 +250,7 @@ function buildClassicSystemPrompt(brand, config) {
|
|
|
246
250
|
'- 3-5 sections — each is ONE topic, rewritten in your voice as original content',
|
|
247
251
|
'- Each section: title (compelling, scannable), body (80-150 words, markdown OK)',
|
|
248
252
|
'- Each section: image_prompt — one-sentence visual description for an illustrator. Be specific about subject/style.',
|
|
249
|
-
'- Do NOT
|
|
253
|
+
'- Do NOT invent URLs or "read more" links. The newsletter is a self-contained read. The ONLY URLs allowed in section bodies are the brand links listed above (if any) — weave them in naturally where relevant, do not force them.',
|
|
250
254
|
`- Signoff: a SHORT human sign-off, formatted as two lines with \\n between them. First line is a closing phrase like "Best,", "Cheers,", "Until next week,", or "Stay sharp,". Second line is the team name like "The ${brand?.name || 'Team'} Team". Example: "Best,\\nThe ${brand?.name || 'Team'} Team". Do NOT write a summary, tagline, motto, or thematic conclusion sentence — this is the literal way you sign off the email, like the end of a letter.`,
|
|
251
255
|
'- citations: array of { note, source } for any hard data referenced. Empty array if none.',
|
|
252
256
|
'',
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Pipeline:
|
|
9
9
|
* 1. Read newsletter categories from Manager.config.marketing.newsletter.content.categories
|
|
10
|
-
* 2. Fetch ready sources from parent server (
|
|
10
|
+
* 2. Fetch ready sources from parent server (no claiming — child tracks locally)
|
|
11
11
|
* 3. structure.js → AI authors subject, preheader, intro, sections, signoff
|
|
12
12
|
* 4. image-illustrator.js → AI generates one flat-vector PNG per section via
|
|
13
13
|
* gpt-image-2 (default). Set content.method.image = 'svg' to use the legacy
|
|
@@ -41,6 +41,7 @@ const { renderMarkdown } = require('./lib/markdown-renderer.js');
|
|
|
41
41
|
const { uploadAssets, RAW_BASE } = require('./lib/image-host.js');
|
|
42
42
|
const { buildPublicConfig } = require('../../../routes/brand/get.js');
|
|
43
43
|
const { writeArticle, publishArticle } = require('../../../libraries/content/ghostii.js');
|
|
44
|
+
const { trackContentSource, contentSourceHash } = require('../../../events/cron/daily/blog-auto-publisher.js');
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* Generate newsletter content from parent server sources.
|
|
@@ -69,38 +70,52 @@ const { writeArticle, publishArticle } = require('../../../libraries/content/gho
|
|
|
69
70
|
*/
|
|
70
71
|
async function generate(Manager, assistant, settings, opts = {}) {
|
|
71
72
|
const newsletterRoleConfig = Manager.config?.marketing?.newsletter;
|
|
72
|
-
const
|
|
73
|
+
const rawContent = newsletterRoleConfig?.content;
|
|
73
74
|
|
|
74
75
|
if (!newsletterRoleConfig?.enabled) {
|
|
75
76
|
assistant.log('Newsletter generator: newsletter disabled in config');
|
|
76
77
|
return null;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
if (!
|
|
80
|
+
if (!rawContent) {
|
|
80
81
|
assistant.log('Newsletter generator: no marketing.newsletter.content config block');
|
|
81
82
|
return null;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
//
|
|
85
|
+
// Content can be an array or single object (matches blog config shape)
|
|
86
|
+
const contentArray = powertools.arrayify(rawContent);
|
|
87
|
+
const config = contentArray[0];
|
|
88
|
+
|
|
89
|
+
if (!config) {
|
|
90
|
+
assistant.log('Newsletter generator: empty content array');
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Default sources to ['$parent'] if not specified
|
|
95
|
+
const contentSources = config.sources || ['$parent'];
|
|
96
|
+
|
|
97
|
+
// Either use pre-fetched sources (iteration test) or resolve from config
|
|
85
98
|
let sources = opts.sources;
|
|
86
99
|
|
|
87
100
|
if (!sources) {
|
|
88
|
-
|
|
101
|
+
// For now, newsletter resolves $parent sources via the parent server
|
|
102
|
+
if (contentSources.includes('$parent')) {
|
|
103
|
+
const categories = config.categories || [];
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
if (!categories.length) {
|
|
106
|
+
assistant.log('Newsletter generator: no categories configured');
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
94
109
|
|
|
95
|
-
|
|
110
|
+
const parentUrl = Manager.getParentApiUrl();
|
|
96
111
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
if (!parentUrl) {
|
|
113
|
+
assistant.log('Newsletter generator: no parent URL configured');
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
101
116
|
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
sources = await fetchSources(parentUrl, categories, assistant);
|
|
118
|
+
}
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
if (!sources?.length) {
|
|
@@ -382,12 +397,20 @@ async function generate(Manager, assistant, settings, opts = {}) {
|
|
|
382
397
|
});
|
|
383
398
|
}
|
|
384
399
|
|
|
385
|
-
// 4.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
400
|
+
// 4. Track all sources in the unified content-sources collection
|
|
401
|
+
const admin = Manager.libraries?.admin;
|
|
402
|
+
if (admin) {
|
|
403
|
+
for (const source of sources) {
|
|
404
|
+
await trackContentSource(admin, {
|
|
405
|
+
url: source.url || source.id,
|
|
406
|
+
origin: '$parent',
|
|
407
|
+
itemId: source.id,
|
|
408
|
+
itemTitle: source.title || '',
|
|
409
|
+
usedBy: 'newsletter',
|
|
410
|
+
brandId: brand?.id || '',
|
|
411
|
+
}).catch((e) => {
|
|
412
|
+
assistant.error(`Newsletter generator: Error tracking content source (non-fatal): ${e.message}`);
|
|
413
|
+
});
|
|
391
414
|
}
|
|
392
415
|
}
|
|
393
416
|
|
|
@@ -675,7 +698,7 @@ function aggregateTotals(filterMeta, structureMeta, images) {
|
|
|
675
698
|
/**
|
|
676
699
|
* Fetch ready newsletter sources from the parent server.
|
|
677
700
|
*/
|
|
678
|
-
async function fetchSources(parentUrl, categories,
|
|
701
|
+
async function fetchSources(parentUrl, categories, assistant) {
|
|
679
702
|
const allSources = [];
|
|
680
703
|
|
|
681
704
|
for (const category of categories) {
|
|
@@ -687,7 +710,6 @@ async function fetchSources(parentUrl, categories, brandId, assistant) {
|
|
|
687
710
|
query: {
|
|
688
711
|
category,
|
|
689
712
|
limit: 3,
|
|
690
|
-
claimFor: brandId,
|
|
691
713
|
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
692
714
|
},
|
|
693
715
|
});
|
|
@@ -703,27 +725,6 @@ async function fetchSources(parentUrl, categories, brandId, assistant) {
|
|
|
703
725
|
return allSources;
|
|
704
726
|
}
|
|
705
727
|
|
|
706
|
-
/**
|
|
707
|
-
* Mark sources as used on the parent server.
|
|
708
|
-
*/
|
|
709
|
-
async function claimSources(parentUrl, sources, brandId, assistant) {
|
|
710
|
-
for (const source of sources) {
|
|
711
|
-
try {
|
|
712
|
-
await fetch(`${parentUrl}/newsletter-sources`, {
|
|
713
|
-
method: 'put',
|
|
714
|
-
response: 'json',
|
|
715
|
-
timeout: 60000,
|
|
716
|
-
body: {
|
|
717
|
-
id: source.id,
|
|
718
|
-
usedBy: brandId || 'unknown',
|
|
719
|
-
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
720
|
-
},
|
|
721
|
-
});
|
|
722
|
-
} catch (e) {
|
|
723
|
-
assistant.error(`Newsletter generator: Failed to claim source ${source.id}: ${e.message}`);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
728
|
|
|
728
729
|
/**
|
|
729
730
|
* Generate a 20-character Firebase push ID (RTDB-style).
|
|
@@ -776,4 +777,4 @@ function generatePushId() {
|
|
|
776
777
|
return id;
|
|
777
778
|
}
|
|
778
779
|
|
|
779
|
-
module.exports = { generate, fetchSources,
|
|
780
|
+
module.exports = { generate, fetchSources, generatePushId };
|
|
@@ -10,7 +10,7 @@ module.exports = async ({ assistant, Manager }) => {
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Build a public-safe config object from Manager.config
|
|
13
|
-
* Excludes sensitive fields: sentry, analytics,
|
|
13
|
+
* Excludes sensitive fields: sentry, analytics, blog, etc.
|
|
14
14
|
*/
|
|
15
15
|
function buildPublicConfig(config) {
|
|
16
16
|
return {
|
|
@@ -146,41 +146,29 @@
|
|
|
146
146
|
enabled: false,
|
|
147
147
|
platform: 'beehiiv',
|
|
148
148
|
publicationId: '', // Beehiiv publication ID (e.g., 'pub_xxxxx'). Populated by OMEGA's beehiiv/ensure/publication.js. Required for marketing sync to Beehiiv.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
sponsorships: [
|
|
173
|
-
// {
|
|
174
|
-
// label: 'Sponsored', // optional eyebrow label (defaults to "Sponsored")
|
|
175
|
-
// headline: 'Grow your audience faster', // bold headline
|
|
176
|
-
// body: 'Short pitch text…', // 1-2 sentence body
|
|
177
|
-
// url: 'https://somiibo.com/promo', // click destination
|
|
178
|
-
// image: 'https://...png', // optional image URL
|
|
179
|
-
// ctaLabel: 'Try Somiibo', // button text (default: "Learn more")
|
|
180
|
-
// position: 'middle', // 'top' | 'middle' | 'end' (default: 'middle')
|
|
181
|
-
// },
|
|
182
|
-
],
|
|
183
|
-
},
|
|
149
|
+
// Content: single object or array (matches blog config shape). Generator uses the first entry.
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
sources: ['$parent'], // '$parent' | '$feed:https://...' | '$brand'
|
|
153
|
+
categories: [], // e.g., ['social-media', 'marketing'] — topic constraints + AI categorization
|
|
154
|
+
links: [], // links to inject into generated content
|
|
155
|
+
instructions: '', // free-form text passed to the AI
|
|
156
|
+
tone: 'professional', // 'professional' | 'casual' | 'actionable' | 'witty' | etc.
|
|
157
|
+
keywords: [], // SEO keywords to weave in
|
|
158
|
+
template: 'clean', // 'clean' | 'editorial' | 'field-report' — layout template
|
|
159
|
+
article: {
|
|
160
|
+
enabled: false, // expand lead section into a full blog post via Ghostii
|
|
161
|
+
author: null,
|
|
162
|
+
},
|
|
163
|
+
theme: {
|
|
164
|
+
primaryColor: '#5B5BFF',
|
|
165
|
+
secondaryColor: '#1E1E2A',
|
|
166
|
+
accentColor: '#F6F7FB',
|
|
167
|
+
font: 'Inter, system-ui, sans-serif',
|
|
168
|
+
},
|
|
169
|
+
sponsorships: [],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
184
172
|
},
|
|
185
173
|
prune: {
|
|
186
174
|
enabled: true,
|
|
@@ -195,48 +183,56 @@
|
|
|
195
183
|
appId: '1:123:web:456',
|
|
196
184
|
measurementId: 'G-0123456789',
|
|
197
185
|
},
|
|
198
|
-
// Standalone
|
|
199
|
-
//
|
|
200
|
-
//
|
|
186
|
+
// Standalone blog auto-publisher (daily cron). OPT-IN: disabled by default.
|
|
187
|
+
// Set enabled: true and quantity >= 1 to auto-publish independent blog posts.
|
|
188
|
+
// For newsletter-linked articles, use marketing.newsletter.content.article.enabled instead.
|
|
189
|
+
//
|
|
190
|
+
// `platform` selects the article-generation provider (only 'ghostii' for now).
|
|
201
191
|
//
|
|
202
192
|
// Source types:
|
|
203
|
-
// '$
|
|
193
|
+
// '$brand' — generic brand-topic generation
|
|
204
194
|
// '$feed:https://example.com/feed' — RSS/Atom/JSON feed: pick one unprocessed article per run
|
|
195
|
+
// '$parent' — fetch sources from parent server's source pool (without claiming)
|
|
205
196
|
// 'https://example.com/page' — fetch URL content as prompt seed
|
|
206
197
|
// 'Write about topic X' — text: use directly as prompt seed
|
|
207
198
|
//
|
|
208
|
-
//
|
|
209
|
-
// is never reprocessed. When a feed is unreachable or exhausted, falls back to $
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// '$feed:https
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
199
|
+
// All feed/parent sources are tracked in Firestore (`content-sources`) so the same
|
|
200
|
+
// article is never reprocessed. When a feed is unreachable or exhausted, falls back to $brand.
|
|
201
|
+
blog: {
|
|
202
|
+
enabled: false,
|
|
203
|
+
platform: 'ghostii',
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
sources: [ // '$brand' | '$feed:https://...' | '$parent'
|
|
207
|
+
'$brand',
|
|
208
|
+
// '$feed:https://techcrunch.com/feed/',
|
|
209
|
+
// '$parent',
|
|
210
|
+
],
|
|
211
|
+
categories: [], // topic constraints + output tags
|
|
212
|
+
links: [], // links to inject into generated content
|
|
213
|
+
instructions: '', // free-form text passed to the AI
|
|
214
|
+
tone: 'professional', // 'professional' | 'casual' | 'actionable' | 'witty' | etc.
|
|
215
|
+
keywords: [], // SEO keywords to weave in
|
|
216
|
+
quantity: 0, // number of articles to generate per run
|
|
217
|
+
chance: 1.0,
|
|
218
|
+
author: null,
|
|
219
|
+
postPath: 'ghostii', // defaults to platform name; sub-folder under _posts/{year}/
|
|
220
|
+
// Per-entry Ghostii API overrides (all optional, fall back to framework defaults)
|
|
221
|
+
overrides: {
|
|
222
|
+
// keywords: ['AI', 'technology'],
|
|
223
|
+
// length: 'long', // short | medium | long | comprehensive
|
|
224
|
+
// research: true, // web search for real external links
|
|
225
|
+
// insertImages: true, // section images from Unsplash
|
|
226
|
+
// headerImageUrl: 'unsplash', // disabled | unsplash | generate
|
|
227
|
+
// maxLinks: 6,
|
|
228
|
+
// sectionQuantity: 5,
|
|
229
|
+
// feedUrl: '', // brand blog feed for title dedup
|
|
230
|
+
},
|
|
231
|
+
// brand: 'other-app-id', // Optional: target a different brand
|
|
232
|
+
// brandUrl: 'https://api.otherapp.com', // Required if brand is set
|
|
235
233
|
},
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
],
|
|
234
|
+
],
|
|
235
|
+
},
|
|
240
236
|
reviews: {
|
|
241
237
|
enabled: true,
|
|
242
238
|
sites: [
|