backend-manager 5.8.3 → 5.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/CHANGELOG.md CHANGED
@@ -14,6 +14,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ # [5.9.1] - 2026-06-20
18
+
19
+ ### Fixed
20
+ - Missing `require('node-powertools')` in newsletter generator — broke `powertools.arrayify()` for array content support added in v5.9.0
21
+
22
+ # [5.9.0] - 2026-06-19
23
+
24
+ ### BREAKING
25
+ - **Config key `ghostii` renamed to `blog`.** Consumer projects must migrate their `backend-manager-config.json`: `ghostii[]` → `blog.content[]` with `blog.enabled` and `blog.platform: 'ghostii'`.
26
+ - **Field renames in blog content entries.** `articles` → `quantity`, `prompt` → `instructions`, `$app` → `$brand`.
27
+ - **Newsletter `claimSources()` removed.** Parent server now serves sources read-only (no claiming). Children track usage locally in `content-sources`.
28
+
29
+ ### Added
30
+ - **Provider-based blog architecture.** `blog.platform` selects the article-generation provider (dispatched via `require(../content/${platform}.js)`). Only `ghostii` for now.
31
+ - **`$parent` source type.** Blog entries can fetch sources from the parent server's pool, filtered by categories, with local dedup tracking.
32
+ - **`$brand` source type.** Renamed from `$app` — generic brand-topic generation.
33
+ - **New content entry fields.** `tone`, `categories`, `keywords` added to both blog and newsletter content entries. Injected into AI prompts for better content targeting.
34
+ - **`links` and `keywords` support in newsletter.** Newsletter AI prompt now weaves in brand links and SEO keywords from content config.
35
+ - **`sources` field on newsletter content.** Newsletter entries now explicitly declare source types (default: `['$parent']`), matching blog config shape.
36
+ - **Newsletter `content` supports array format.** Matches blog config shape for structural parity.
37
+ - **Unified `content-sources` Firestore collection.** Both blog and newsletter track used sources in the same collection with `usedBy` field (`'blog'` or `'newsletter'`).
38
+
39
+ ### Changed
40
+ - **Cron renamed.** `ghostii-auto-publisher.js` → `blog-auto-publisher.js`.
41
+ - **`postPath` defaults to platform name.** Instead of hardcoded `'blog'`, defaults to the platform value (e.g. `'ghostii'`).
42
+ - **Consistent field ordering.** `sources → categories → links → instructions → tone → keywords` across blog and newsletter config templates.
43
+
44
+ ### Removed
45
+ - **`ghostii-auto-publisher.js`** — replaced by `blog-auto-publisher.js`.
46
+ - **`claimSources()` from newsletter generator** — parent no longer tracks claim state.
47
+
17
48
  # [5.8.3] - 2026-06-19
18
49
 
19
50
  ### Fixed
package/CLAUDE.md CHANGED
@@ -162,7 +162,7 @@ Deep references live in `docs/`. **Whenever you make a behavioral change, update
162
162
 
163
163
  ### Subsystems & Libraries
164
164
 
165
- - [docs/ghostii.md](docs/ghostii.md) — Ghostii article publisher: source types (`$app` / `$feed:` / URL / text), per-entry API overrides, RSS/Atom feed parser, Firestore feed-item tracking, `sourceContent` pass-through to Ghostii API
165
+ - [docs/ghostii.md](docs/ghostii.md) — Blog auto-publisher (Ghostii provider): source types (`$brand` / `$feed:` / `$parent` / URL / text), provider-based architecture, per-entry API overrides, RSS/Atom feed parser, unified `content-sources` Firestore tracking, `sourceContent` pass-through to Ghostii API
166
166
  - [docs/email-system.md](docs/email-system.md) — unified MJML email rendering pipeline: shared preparation (`prepare.js`), composable template system (`base.js` blocks), 4 email templates (card, plain, order, feedback), no SendGrid dynamic templates — everything rendered server-side
167
167
  - [docs/usage-rate-limiting.md](docs/usage-rate-limiting.md) — usage tracking, monthly/daily caps, `setUser()` + mirrors for proxy usage, reset schedule
168
168
  - [docs/ai-library.md](docs/ai-library.md) — `Manager.AI()` unified entry for OpenAI + Anthropic (text via `.request()`, images via `.image()` → `gpt-image-2`)
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `POST /admin/post` route creates blog posts via GitHub's API. It handles image extraction, resize, upload, and body rewriting.
4
4
 
5
- **Consumers.** Besides direct admin/blogger HTTP calls, this route is the publish target for the **Ghostii article engine** (`src/manager/libraries/content/ghostii.js` → `publishArticle()`), which is used by two paths: the standalone daily `ghostii-auto-publisher.js` cron (off by default), and the newsletter generator's linked-article flow (`marketing.newsletter.content.article.enabled`). Both POST `title`/`url`/`description`/`headerImageURL`/`body`/`author`/`categories`/`tags`/`postPath`/`githubUser`/`githubRepo` with a `backendManagerKey`. Note: `headerImageURL` MUST resolve to a `.jpg` (the downloader rejects non-`.jpg` headers); Ghostii's `unsplash` hero satisfies this. See [docs/marketing-campaigns.md](marketing-campaigns.md).
5
+ **Consumers.** Besides direct admin/blogger HTTP calls, this route is the publish target for the **Ghostii article engine** (`src/manager/libraries/content/ghostii.js` → `publishArticle()`), which is used by two paths: the standalone daily `blog-auto-publisher.js` cron (off by default), and the newsletter generator's linked-article flow (`marketing.newsletter.content.article.enabled`). Both POST `title`/`url`/`description`/`headerImageURL`/`body`/`author`/`categories`/`tags`/`postPath`/`githubUser`/`githubRepo` with a `backendManagerKey`. Note: `headerImageURL` MUST resolve to a `.jpg` (the downloader rejects non-`.jpg` headers); Ghostii's `unsplash` hero satisfies this. See [docs/marketing-campaigns.md](marketing-campaigns.md).
6
6
 
7
7
  **Ghostii is unopinionated about BEM.** Its `/write/article` response is a generic article — a `json` block array (`[{ name, content }]` where name ∈ `heading-1..6`/`image`/`paragraph`/`blockquote`/`list`) plus top-level `title`/`description`/`headerImageUrl`/`images`/`categories`/`keywords`. BEM owns the transform into this route's shape: `blocksToPost(article.json)` extracts the `heading-1` as the title, the first `image` block as `headerImageURL`, and joins every remaining block as the `body` (content only — NO title, NO header image embedded, since this route adds those itself; section images stay in the body and are extracted normally). Older Ghostii responses without `json` fall back to the flat `article.{title,body,headerImageUrl}` fields.
8
8
 
package/docs/ghostii.md CHANGED
@@ -1,8 +1,8 @@
1
- # Ghostii Article Publisher
1
+ # Blog Auto-Publisher (Ghostii Provider)
2
2
 
3
- The Ghostii system generates and publishes blog posts via the daily `bm_cronDaily` cron job. Two independent paths:
3
+ The blog system generates and publishes blog posts via the daily `bm_cronDaily` cron job. Two independent paths:
4
4
 
5
- 1. **Standalone publisher** — `config.ghostii[]` entries: daily cron picks sources, calls the Ghostii AI API, publishes via `admin/post`.
5
+ 1. **Standalone publisher** — `config.blog` with `enabled: true`: daily cron picks sources, calls the configured platform provider (default: Ghostii AI API), publishes via `admin/post`.
6
6
  2. **Newsletter-linked articles** — `marketing.newsletter.content.article.enabled`: the newsletter generator expands its lead section into a full blog post via Ghostii.
7
7
 
8
8
  This doc covers the standalone publisher. For newsletter integration, see [marketing-campaigns.md](marketing-campaigns.md).
@@ -10,59 +10,71 @@ This doc covers the standalone publisher. For newsletter integration, see [marke
10
10
  ## Architecture
11
11
 
12
12
  ```
13
- config.ghostii[] entry
14
-
15
- ghostii-auto-publisher cron (daily)
16
- (per entry, per article)
17
- resolveSource() detect source type, fetch/parse if needed
18
-
19
- writeArticle() call Ghostii AI API (api.ghostii.ai/write/article)
20
-
21
- publishArticle() POST to admin/post GitHub commit
22
-
23
- trackFeedItem() Firestore (for $feed: sources only)
13
+ config.blog.content[] entry
14
+ |
15
+ blog-auto-publisher cron (daily)
16
+ | (per entry, per article)
17
+ resolveSource() -- detect source type, fetch/parse if needed
18
+ |
19
+ provider.writeArticle() -- call provider API (e.g. Ghostii: api.ghostii.ai/write/article)
20
+ |
21
+ provider.publishArticle() -- POST to admin/post -> GitHub commit
22
+ |
23
+ trackContentSource() -- Firestore (for $feed:, $parent sources)
24
24
  ```
25
25
 
26
26
  ### Key files
27
27
 
28
28
  | File | Purpose |
29
29
  |---|---|
30
- | `src/manager/libraries/content/ghostii.js` | `writeArticle()`, `publishArticle()`, `blocksToPost()` API client + post transform |
31
- | `src/manager/libraries/content/feed-parser.js` | `parseFeed()`, `extractArticleContent()` RSS/Atom/JSON parser + article extractor |
32
- | `src/manager/events/cron/daily/ghostii-auto-publisher.js` | Daily cron: source resolution, feed processing, Firestore tracking |
30
+ | `src/manager/libraries/content/ghostii.js` | `writeArticle()`, `publishArticle()`, `blocksToPost()` -- Ghostii API client + post transform |
31
+ | `src/manager/libraries/content/feed-parser.js` | `parseFeed()`, `extractArticleContent()` -- RSS/Atom/JSON parser + article extractor |
32
+ | `src/manager/events/cron/daily/blog-auto-publisher.js` | Daily cron: source resolution, feed processing, Firestore tracking, provider dispatch |
33
33
  | `src/manager/routes/admin/post/post.js` | Publishing endpoint: image download + resize, GitHub commit |
34
34
 
35
35
  ## Source Types
36
36
 
37
- Each `ghostii[]` entry has a `sources` array. The cron picks one at random per article.
37
+ Each `blog.content[]` entry has a `sources` array. The cron picks one at random per article.
38
38
 
39
39
  | Source | Format | Behavior |
40
40
  |---|---|---|
41
- | Generic topic | `'$app'` | Prompts Ghostii to write about any topic relevant to the brand |
42
- | RSS/Atom feed | `'$feed:https://example.com/feed/'` | Parses feed, picks one unprocessed article, extracts content, passes as `sourceContent` to Ghostii |
41
+ | Generic topic | `'$brand'` | Prompts the provider to write about any topic relevant to the brand |
42
+ | RSS/Atom feed | `'$feed:https://example.com/feed/'` | Parses feed, picks one unprocessed article, extracts content, passes as `sourceContent` to the provider |
43
+ | Parent server | `'$parent'` | Fetches sources from parent server's source pool (without claiming), filters out already-used sources locally |
43
44
  | URL | `'https://example.com/page'` | Fetches page HTML, extracts body text, uses as prompt seed |
44
45
  | Text | `'Write about blockchain'` | Uses directly as prompt seed |
45
46
 
46
47
  ### Feed source flow
47
48
 
48
49
  1. Fetch and parse the RSS/Atom/JSON feed via `feed-parser.parseFeed()`
49
- 2. Query `ghostii-sources` in Firestore for already-processed items
50
+ 2. Query `content-sources` in Firestore for already-processed items
50
51
  3. Filter to unprocessed items, pick the newest
51
52
  4. Extract full article content from the item URL via `feed-parser.extractArticleContent()`
52
- 5. Pass extracted text as `sourceContent` to the Ghostii API (separate from the `description` prompt)
53
- 6. After publish, write a tracking doc to `ghostii-sources/{hash}`
54
- 7. On failure (feed unreachable, unparseable, exhausted): fall back to `$app` behavior
53
+ 5. Pass extracted text as `sourceContent` to the provider API (separate from the `description` prompt)
54
+ 6. After publish, write a tracking doc to `content-sources/{hash}`
55
+ 7. On failure (feed unreachable, unparseable, exhausted): fall back to `$brand` behavior
55
56
 
56
- ### Feed item tracking
57
+ ### Parent source flow
57
58
 
58
- Collection: `ghostii-sources` (consumer project Firestore)
59
+ 1. Fetch available sources from parent server via `GET {parentUrl}/newsletter-sources` (no `claimFor` -- read-only)
60
+ 2. Query `content-sources` in Firestore for already-used sources with `origin: '$parent'`
61
+ 3. Filter out already-used sources, pick the first available
62
+ 4. On failure (no parent URL, no sources, all exhausted): fall back to `$brand` behavior
63
+
64
+ ### Content source tracking
65
+
66
+ Collection: `content-sources` (consumer project Firestore)
67
+
68
+ Unified tracker for all source types -- shared between blog-auto-publisher and newsletter generator.
59
69
 
60
70
  ```js
61
- ghostii-sources/{sha256(feedUrl + '::' + itemId).slice(0,20)}: {
62
- feedUrl: 'https://...',
71
+ content-sources/{sha256(origin + '::' + url).slice(0,20)}: {
72
+ url: 'https://...', // unique identifier (article URL or source ID)
73
+ origin: '$feed:https://...', // or '$parent', '$brand'
74
+ feedUrl: 'https://...', // for feed sources
63
75
  itemId: 'guid-or-url',
64
- itemUrl: 'https://...',
65
76
  itemTitle: '...',
77
+ usedBy: 'blog', // or 'newsletter'
66
78
  brandId: '...',
67
79
  postUrl: '...',
68
80
  postSlug: '...',
@@ -70,7 +82,6 @@ ghostii-sources/{sha256(feedUrl + '::' + itemId).slice(0,20)}: {
70
82
  created: { timestamp: '...ISO...', timestampUNIX: 1234567890 },
71
83
  updated: { timestamp: '...ISO...', timestampUNIX: 1234567890 },
72
84
  },
73
- metadata: { created: Timestamp },
74
85
  }
75
86
  ```
76
87
 
@@ -78,53 +89,67 @@ ghostii-sources/{sha256(feedUrl + '::' + itemId).slice(0,20)}: {
78
89
 
79
90
  ```js
80
91
  // backend-manager-config.json
81
- ghostii: [
82
- {
83
- articles: 1, // articles per cron run (0 = disabled)
84
- sources: ['$app'], // source pool (mixed types OK)
85
- links: [], // URLs to inject into article body
86
- prompt: '', // custom instructions for the AI
87
- chance: 1.0, // probability of running (0-1)
88
- author: null, // author slug (null = admin/post default)
89
- postPath: 'ghostii', // sub-folder under _posts/{year}/
90
- overrides: { // per-entry Ghostii API param overrides
91
- // keywords: ['AI'],
92
- // length: 'long', // short | medium | long | comprehensive
93
- // research: true, // web search for real links
94
- // insertImages: true,
95
- // insertLinks: true,
96
- // headerImageUrl: 'unsplash', // disabled | unsplash | generate
97
- // maxLinks: 6,
98
- // sectionQuantity: 5,
99
- // feedUrl: '', // brand blog feed (title dedup)
100
- },
101
- // brand: 'other-brand-id', // cross-brand publishing
102
- // brandUrl: 'https://api.other.com',
103
- }
104
- ]
92
+ blog: {
93
+ enabled: false,
94
+ platform: 'ghostii', // provider: only 'ghostii' for now
95
+ content: [
96
+ {
97
+ quantity: 1, // articles per cron run (0 = disabled)
98
+ sources: ['$brand'], // source pool (mixed types OK)
99
+ links: [], // URLs to inject into article body
100
+ instructions: '', // custom instructions for the AI
101
+ tone: 'professional', // 'professional', 'casual', 'actionable', etc.
102
+ categories: [], // topic constraints + output tags
103
+ keywords: [], // SEO keywords to weave in
104
+ chance: 1.0, // probability of running (0-1)
105
+ author: null, // author slug (null = admin/post default)
106
+ postPath: 'blog', // sub-folder under _posts/{year}/
107
+ overrides: { // per-entry Ghostii API param overrides
108
+ // keywords: ['AI'],
109
+ // length: 'long', // short | medium | long | comprehensive
110
+ // research: true, // web search for real links
111
+ // insertImages: true,
112
+ // insertLinks: true,
113
+ // headerImageUrl: 'unsplash', // disabled | unsplash | generate
114
+ // maxLinks: 6,
115
+ // sectionQuantity: 5,
116
+ // feedUrl: '', // brand blog feed (title dedup)
117
+ },
118
+ // brand: 'other-brand-id', // cross-brand publishing
119
+ // brandUrl: 'https://api.other.com',
120
+ }
121
+ ],
122
+ }
105
123
  ```
106
124
 
107
125
  ### Per-entry overrides
108
126
 
109
127
  All Ghostii API params were previously hardcoded in `writeArticle()`. They now accept per-entry overrides via the `overrides` object. Unspecified values fall back to framework defaults (length: 'long', research: true, etc.).
110
128
 
111
- ### `sourceContent` — Ghostii API field
129
+ ### Keywords resolution
130
+
131
+ Keywords are resolved in this order:
132
+ 1. `overrides.keywords` -- per-entry Ghostii API override (highest priority)
133
+ 2. `entry.keywords` -- top-level entry keywords (used if overrides.keywords is not set)
134
+ 3. Empty array -- framework default
135
+
136
+ ### `sourceContent` -- Ghostii API field
112
137
 
113
- When a `$feed:` source provides extracted article text, it's passed to the Ghostii API as `sourceContent` (max 16KB). This is separate from `description` (the editorial brief, max 2KB). Ghostii's title generation, outline, and section writing prompts reference the source content as context for writing original articles not rewrites.
138
+ When a `$feed:` or `$parent` source provides extracted article text, it's passed to the Ghostii API as `sourceContent` (max 16KB). This is separate from `description` (the editorial brief, max 2KB). Ghostii's title generation, outline, and section writing prompts reference the source content as context for writing original articles -- not rewrites.
114
139
 
115
140
  ## Feed Parser
116
141
 
117
142
  `src/manager/libraries/content/feed-parser.js` exports:
118
143
 
119
- - **`parseFeed(text)`** Parse RSS 2.0, Atom 1.0, or JSON Feed into `{ items: [{ id, title, url, summary, content, publishedAt }] }`. Handles CDATA, namespaced elements (`content:encoded`), BOM, attribute-based links. Returns `{ items: [] }` on invalid input.
144
+ - **`parseFeed(text)`** -- Parse RSS 2.0, Atom 1.0, or JSON Feed into `{ items: [{ id, title, url, summary, content, publishedAt }] }`. Handles CDATA, namespaced elements (`content:encoded`), BOM, attribute-based links. Returns `{ items: [] }` on invalid input.
120
145
 
121
- - **`extractArticleContent(url)`** Fetch URL, extract readable text from `<article>` `<main>` `<body>`, strip scripts/styles/nav/footer/header/aside, normalize whitespace, truncate to 14KB.
146
+ - **`extractArticleContent(url)`** -- Fetch URL, extract readable text from `<article>` -> `<main>` -> `<body>`, strip scripts/styles/nav/footer/header/aside, normalize whitespace, truncate to 14KB.
122
147
 
123
148
  ## Tests
124
149
 
125
150
  ```bash
126
- npx mgr test bem:helpers/content/feed-parser # 32 tests: RSS/Atom/JSON parsing, edge cases, HTML extraction
151
+ npx mgr test bem:helpers/content/feed-parser # 32 tests: RSS/Atom/JSON parsing, edge cases, HTML extraction
127
152
  npx mgr test bem:helpers/content/ghostii-write-article # 15 tests: override pass-through, sourceContent, backwards compat
128
- npx mgr test bem:helpers/content/ghostii-auto-publisher # 20 tests: source detection, feed processing, Firestore tracking
153
+ npx mgr test bem:helpers/content/blog-auto-publisher # 25+ tests: source detection, feed processing, Firestore tracking
129
154
  npx mgr test bem:helpers/content/ghostii-blocks # 8 tests: blocksToPost() (unchanged)
130
155
  ```
@@ -174,7 +174,7 @@ Flow (runs in parallel with SVG generation, between structure and render):
174
174
 
175
175
  **Failure is isolated** — if the article build throws, it resolves to `null`, no CTA is injected, and the newsletter ships normally. The newsletter never depends on the article succeeding.
176
176
 
177
- The published URL is surfaced on the return as `assets.articleUrl` and `meta.article`. Config block: `marketing.newsletter.content.article = { enabled, author }` (`enabled` default `false`; `author` is the post author slug). Standalone article publishing (independent of the newsletter) still lives in the daily `ghostii-auto-publisher.js` cron, which now shares the same `libraries/content/ghostii.js` engine — see [docs/admin-post-route.md](admin-post-route.md). Standalone Ghostii is **disabled by default** (`ghostii[0].articles: 0`).
177
+ The published URL is surfaced on the return as `assets.articleUrl` and `meta.article`. Config block: `marketing.newsletter.content.article = { enabled, author }` (`enabled` default `false`; `author` is the post author slug). Standalone article publishing (independent of the newsletter) lives in the daily `blog-auto-publisher.js` cron, which uses the same `libraries/content/ghostii.js` engine — see [docs/admin-post-route.md](admin-post-route.md). Standalone blog publishing is **disabled by default** (`blog.enabled: false`).
178
178
 
179
179
  ## Template-owned schemas
180
180
 
@@ -360,7 +360,7 @@ marketing: {
360
360
  | Newsletter asset host (GitHub upload — PNGs + newsletter.html + newsletter.md + summary.md) | `src/manager/libraries/email/generators/lib/image-host.js` |
361
361
  | Newsletter markdown renderer (programmatic, no AI) | `src/manager/libraries/email/generators/lib/markdown-renderer.js` |
362
362
  | Ghostii article engine (writeArticle + publishArticle) | `src/manager/libraries/content/ghostii.js` |
363
- | Standalone Ghostii article cron (off by default) | `src/manager/events/cron/daily/ghostii-auto-publisher.js` |
363
+ | Standalone blog article cron (off by default) | `src/manager/events/cron/daily/blog-auto-publisher.js` |
364
364
  | Unified AI library | `src/manager/libraries/ai/index.js` (OpenAI + Anthropic via `Manager.AI(assistant).request({ provider, ... })`) |
365
365
  | Notification library | `src/manager/libraries/notification.js` |
366
366
  | SendGrid provider | `src/manager/libraries/email/providers/sendgrid.js` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.8.3",
3
+ "version": "5.9.1",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {