openwriter 0.40.1 → 0.40.2

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.
Files changed (56) hide show
  1. package/dist/plugins/authors-voice/dist/index.d.ts +48 -0
  2. package/dist/plugins/authors-voice/dist/index.js +235 -0
  3. package/dist/plugins/authors-voice/package.json +24 -0
  4. package/dist/plugins/authors-voice/skill/LICENSE +21 -0
  5. package/dist/plugins/authors-voice/skill/README.md +126 -0
  6. package/dist/plugins/authors-voice/skill/SKILL.md +151 -0
  7. package/dist/plugins/authors-voice/skill/catalog/ai-tells.md +144 -0
  8. package/dist/plugins/authors-voice/skill/catalog/anchor-prompt.md +189 -0
  9. package/dist/plugins/authors-voice/skill/catalog/author-hints.md +119 -0
  10. package/dist/plugins/authors-voice/skill/catalog/fingerprints.md +175 -0
  11. package/dist/plugins/authors-voice/skill/catalog/hurdle.md +76 -0
  12. package/dist/plugins/authors-voice/skill/catalog/post-write-audit.md +105 -0
  13. package/dist/plugins/authors-voice/skill/docs/analysis.md +31 -0
  14. package/dist/plugins/authors-voice/skill/docs/anchor-iteration.md +176 -0
  15. package/dist/plugins/authors-voice/skill/docs/api/import.md +78 -0
  16. package/dist/plugins/authors-voice/skill/docs/api/protocol.md +140 -0
  17. package/dist/plugins/authors-voice/skill/docs/api/setup.md +37 -0
  18. package/dist/plugins/authors-voice/skill/docs/api/tools.md +102 -0
  19. package/dist/plugins/authors-voice/skill/docs/api/troubleshooting.md +7 -0
  20. package/dist/plugins/authors-voice/skill/docs/apply-protocol-deep.md +191 -0
  21. package/dist/plugins/authors-voice/skill/docs/context-hygiene.md +33 -0
  22. package/dist/plugins/authors-voice/skill/docs/setup.md +74 -0
  23. package/dist/plugins/authors-voice/skill/docs/tiers.md +13 -0
  24. package/dist/plugins/authors-voice/skill/package.json +35 -0
  25. package/dist/plugins/authors-voice/skill/prompts/skeleton.md +29 -0
  26. package/dist/plugins/authors-voice/skill/voice/README.md +51 -0
  27. package/dist/plugins/authors-voice/skill/voice/corpus/.gitkeep +0 -0
  28. package/dist/plugins/github/dist/blog-tools.d.ts +84 -0
  29. package/dist/plugins/github/dist/blog-tools.js +1208 -0
  30. package/dist/plugins/github/dist/git-sync.d.ts +46 -0
  31. package/dist/plugins/github/dist/git-sync.js +335 -0
  32. package/dist/plugins/github/dist/helpers.d.ts +127 -0
  33. package/dist/plugins/github/dist/helpers.js +67 -0
  34. package/dist/plugins/github/dist/index.d.ts +12 -0
  35. package/dist/plugins/github/dist/index.js +112 -0
  36. package/dist/plugins/github/package.json +24 -0
  37. package/dist/plugins/image-gen/dist/index.d.ts +35 -0
  38. package/dist/plugins/image-gen/dist/index.js +149 -0
  39. package/dist/plugins/image-gen/package.json +26 -0
  40. package/dist/plugins/publish/dist/helpers.d.ts +66 -0
  41. package/dist/plugins/publish/dist/helpers.js +199 -0
  42. package/dist/plugins/publish/dist/index.d.ts +3 -0
  43. package/dist/plugins/publish/dist/index.js +1156 -0
  44. package/dist/plugins/publish/dist/newsletter-tools.d.ts +2 -0
  45. package/dist/plugins/publish/dist/newsletter-tools.js +394 -0
  46. package/dist/plugins/publish/package.json +31 -0
  47. package/dist/plugins/x-api/dist/index.d.ts +27 -0
  48. package/dist/plugins/x-api/dist/index.js +368 -0
  49. package/dist/plugins/x-api/dist/server-bridge.d.ts +22 -0
  50. package/dist/plugins/x-api/dist/server-bridge.js +43 -0
  51. package/dist/plugins/x-api/package.json +27 -0
  52. package/package.json +1 -1
  53. package/skill/docs/enrichment.md +180 -0
  54. package/skill/docs/footnotes.md +178 -0
  55. package/skill/docs/setup.md +62 -0
  56. package/skill/docs/welcome.md +21 -0
@@ -0,0 +1,112 @@
1
+ /**
2
+ * GitHub Plugin for OpenWriter.
3
+ * Provides two capabilities:
4
+ * 1. Docs backup sync — lifted from core. Auth: existing `gh auth`.
5
+ * 2. Blog sites (Phase 3+) — list of blog repos, post via local git ops.
6
+ *
7
+ * This phase ships capability 1 only. Routes match the previous core mount
8
+ * points exactly so SyncButton + SyncSetupModal continue to work unchanged.
9
+ */
10
+ import { getServerModules } from './helpers.js';
11
+ import { getSyncStatus, getCapabilities, getPendingFiles, setupWithGh, setupWithPat, connectExisting, pushSync, } from './git-sync.js';
12
+ import { blogTools } from './blog-tools.js';
13
+ const plugin = {
14
+ name: '@openwriter/plugin-github',
15
+ version: '0.1.0',
16
+ description: 'GitHub Plugin: Sync files and publish blogs.',
17
+ category: 'productivity',
18
+ // No user-input credentials — auth piggybacks on the user's existing
19
+ // `gh auth` (verified in getCapabilities). Blog-site list is stored
20
+ // outside the configSchema as structured data (Phase 3+).
21
+ configSchema: {},
22
+ mcpTools() {
23
+ return blogTools();
24
+ },
25
+ registerRoutes(ctx) {
26
+ const broadcast = async (status) => {
27
+ try {
28
+ const srv = await getServerModules();
29
+ srv.broadcastSyncStatus(status);
30
+ }
31
+ catch (err) {
32
+ console.error('[GitHub Plugin] broadcast failed:', err.message);
33
+ }
34
+ };
35
+ // MCP-15: log the real error server-side; return a generic message to the
36
+ // client so internal details (paths, git output, tokens) never leak.
37
+ const fail = (res, where, err, extra = {}) => {
38
+ console.error(`[GitHub Plugin] ${where} failed:`, err?.message || err);
39
+ res.status(500).json({ error: 'Internal error', ...extra });
40
+ };
41
+ ctx.app.get('/api/sync/status', async (_req, res) => {
42
+ try {
43
+ res.json(await getSyncStatus());
44
+ }
45
+ catch (err) {
46
+ fail(res, 'sync/status', err, { state: 'error' });
47
+ }
48
+ });
49
+ ctx.app.get('/api/sync/capabilities', async (_req, res) => {
50
+ try {
51
+ res.json(await getCapabilities());
52
+ }
53
+ catch (err) {
54
+ fail(res, 'sync/capabilities', err);
55
+ }
56
+ });
57
+ ctx.app.get('/api/sync/pending', async (_req, res) => {
58
+ try {
59
+ res.json(await getPendingFiles());
60
+ }
61
+ catch (err) {
62
+ fail(res, 'sync/pending', err);
63
+ }
64
+ });
65
+ ctx.app.post('/api/sync/setup', async (req, res) => {
66
+ try {
67
+ const { method, repoName, remoteUrl, pat, isPrivate } = req.body;
68
+ if (method === 'gh') {
69
+ await setupWithGh(repoName || 'openwriter-docs', isPrivate !== false);
70
+ }
71
+ else if (method === 'pat') {
72
+ if (!pat) {
73
+ res.status(400).json({ error: 'PAT is required' });
74
+ return;
75
+ }
76
+ await setupWithPat(pat, repoName || 'openwriter-docs', isPrivate !== false);
77
+ }
78
+ else if (method === 'connect') {
79
+ if (!remoteUrl) {
80
+ res.status(400).json({ error: 'Remote URL is required' });
81
+ return;
82
+ }
83
+ await connectExisting(remoteUrl, pat);
84
+ }
85
+ else {
86
+ res.status(400).json({ error: 'Invalid method. Use: gh, pat, or connect' });
87
+ return;
88
+ }
89
+ const status = await getSyncStatus();
90
+ await broadcast(status);
91
+ res.json({ success: true, status });
92
+ }
93
+ catch (err) {
94
+ // Setup surfaces GitHub's own API feedback (e.g. "name already exists",
95
+ // "bad credentials") which is user-actionable; the remote is now
96
+ // credential-free so this carries no secret. Still log the full error.
97
+ console.error('[GitHub Plugin] sync/setup failed:', err?.message || err);
98
+ res.status(500).json({ error: err?.message || 'Setup failed' });
99
+ }
100
+ });
101
+ ctx.app.post('/api/sync/push', async (_req, res) => {
102
+ try {
103
+ const result = await pushSync((s) => { void broadcast(s); });
104
+ res.json(result);
105
+ }
106
+ catch (err) {
107
+ fail(res, 'sync/push', err, { state: 'error' });
108
+ }
109
+ });
110
+ },
111
+ };
112
+ export default plugin;
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@openwriter/plugin-github",
3
+ "version": "0.1.0",
4
+ "description": "GitHub Plugin: Sync files and publish blogs.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch"
10
+ },
11
+ "dependencies": {},
12
+ "devDependencies": {
13
+ "@types/express": "^5.0.0",
14
+ "typescript": "^5.6.0"
15
+ },
16
+ "openwriter": {
17
+ "displayName": "GitHub",
18
+ "category": "productivity"
19
+ },
20
+ "files": [
21
+ "dist/",
22
+ "package.json"
23
+ ]
24
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Image Generation plugin for OpenWriter.
3
+ * Right-click an empty paragraph → "Generate image" → AI creates an image inline.
4
+ * Uses Google Gemini (Nano Banana 2) for generation, saves to /_images/.
5
+ */
6
+ import type { Express } from 'express';
7
+ interface PluginConfigField {
8
+ type: 'string' | 'number' | 'boolean';
9
+ required?: boolean;
10
+ env?: string;
11
+ description?: string;
12
+ }
13
+ interface PluginRouteContext {
14
+ app: Express;
15
+ config: Record<string, string>;
16
+ dataDir: string;
17
+ }
18
+ interface PluginContextMenuItem {
19
+ label: string;
20
+ shortcut?: string;
21
+ action: string;
22
+ condition?: 'has-selection' | 'empty-node' | 'always';
23
+ promptForInput?: boolean;
24
+ }
25
+ interface OpenWriterPlugin {
26
+ name: string;
27
+ version: string;
28
+ description?: string;
29
+ category?: 'writing' | 'social-media' | 'image-generation';
30
+ configSchema?: Record<string, PluginConfigField>;
31
+ registerRoutes?(ctx: PluginRouteContext): void | Promise<void>;
32
+ contextMenuItems?(): PluginContextMenuItem[];
33
+ }
34
+ declare const plugin: OpenWriterPlugin;
35
+ export default plugin;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Image Generation plugin for OpenWriter.
3
+ * Right-click an empty paragraph → "Generate image" → AI creates an image inline.
4
+ * Uses Google Gemini (Nano Banana 2) for generation, saves to /_images/.
5
+ */
6
+ import { join } from 'path';
7
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
8
+ import { randomUUID } from 'crypto';
9
+ import { homedir } from 'os';
10
+ /** Fallback: generate image via the publish platform API, download and save locally */
11
+ async function generateViaPlatform(prompt, dataDir, imageApiKey, aspectRatio = '16:9') {
12
+ const configPath = join(homedir(), '.openwriter', 'config.json');
13
+ if (!existsSync(configPath))
14
+ return null;
15
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
16
+ const publishConfig = config.plugins?.['@openwriter/plugin-publish']?.config || {};
17
+ const platformKey = publishConfig['api-key'];
18
+ const apiUrl = publishConfig['api-url'] || 'https://publish.openwriter.io';
19
+ const profile = config.activeProfile || 'Default';
20
+ if (!platformKey)
21
+ return null;
22
+ console.log(`[ImageGen] Generating image (platform): "${prompt.slice(0, 80)}..."`);
23
+ const res = await fetch(`${apiUrl}/images/generate`, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ Authorization: `Bearer ${platformKey}`,
28
+ 'X-Profile': profile,
29
+ // BYO image key → worker skips the shared-key allotment and bills the user's own key (uncapped).
30
+ ...(imageApiKey ? { 'X-Image-Key': imageApiKey } : {}),
31
+ },
32
+ body: JSON.stringify({ prompt, aspect_ratio: aspectRatio }),
33
+ });
34
+ if (!res.ok) {
35
+ const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
36
+ throw new Error(err.error || 'Platform image generation failed');
37
+ }
38
+ const data = await res.json();
39
+ // Download image and save locally
40
+ const imageRes = await fetch(data.url);
41
+ const imageBuffer = Buffer.from(await imageRes.arrayBuffer());
42
+ const imagesDir = join(dataDir, '_images');
43
+ if (!existsSync(imagesDir))
44
+ mkdirSync(imagesDir, { recursive: true });
45
+ const filename = `${randomUUID().slice(0, 8)}.png`;
46
+ writeFileSync(join(imagesDir, filename), imageBuffer);
47
+ console.log(`[ImageGen] Saved (platform): ${join(imagesDir, filename)}`);
48
+ return { success: true, src: `/_images/${filename}` };
49
+ }
50
+ const plugin = {
51
+ name: '@openwriter/plugin-image-gen',
52
+ version: '0.1.0',
53
+ description: 'Generate images with AI — right-click empty paragraphs',
54
+ category: 'image-generation',
55
+ configSchema: {
56
+ 'gemini-api-key': {
57
+ type: 'string',
58
+ env: 'GEMINI_API_KEY',
59
+ required: false,
60
+ description: 'Google Gemini API key for image generation (optional — falls back to publish platform)',
61
+ },
62
+ imageApiKey: {
63
+ type: 'string',
64
+ required: false,
65
+ description: "Your own image API key (Gemini) — unlimited generations at your cost. Leave blank to use OpenWriter's included allotment.",
66
+ },
67
+ },
68
+ registerRoutes(ctx) {
69
+ ctx.app.post('/api/image-gen/generate', async (req, res) => {
70
+ try {
71
+ const { prompt } = req.body;
72
+ if (!prompt || typeof prompt !== 'string') {
73
+ res.status(400).json({ success: false, error: 'prompt is required' });
74
+ return;
75
+ }
76
+ if (prompt.length > 1000) {
77
+ res.status(400).json({ success: false, error: 'prompt must be under 1000 characters' });
78
+ return;
79
+ }
80
+ const apiKey = ctx.config['gemini-api-key'] || process.env.GEMINI_API_KEY || '';
81
+ let imageBytes;
82
+ if (apiKey) {
83
+ // Local Gemini generation
84
+ const { GoogleGenAI } = await import('@google/genai');
85
+ const ai = new GoogleGenAI({ apiKey });
86
+ console.log(`[ImageGen] Generating image (local): "${prompt.slice(0, 80)}..."`);
87
+ const response = await ai.models.generateContent({
88
+ model: 'gemini-3.1-flash-image-preview',
89
+ contents: `Generate a 16:9 aspect ratio image: ${prompt}`,
90
+ config: {
91
+ responseModalities: ['IMAGE'],
92
+ },
93
+ });
94
+ const parts = response.candidates?.[0]?.content?.parts;
95
+ if (!parts || parts.length === 0) {
96
+ res.status(422).json({ success: false, error: 'No image generated — content may have been filtered' });
97
+ return;
98
+ }
99
+ const imagePart = parts.find((p) => p.inlineData);
100
+ imageBytes = imagePart?.inlineData?.data;
101
+ if (!imageBytes) {
102
+ res.status(422).json({ success: false, error: 'No image data in response' });
103
+ return;
104
+ }
105
+ }
106
+ else {
107
+ // Fallback: generate via publish platform API.
108
+ // Pass the user's own image key (if set) so the worker bills it instead of the shared allotment.
109
+ const platformResult = await generateViaPlatform(prompt, ctx.dataDir, ctx.config['imageApiKey']);
110
+ if (!platformResult) {
111
+ res.status(400).json({ success: false, error: 'No GEMINI_API_KEY and publish platform not configured. Set GEMINI_API_KEY or log in to the publish plugin.' });
112
+ return;
113
+ }
114
+ // platformResult already saved to disk
115
+ res.json(platformResult);
116
+ return;
117
+ }
118
+ // Save to dataDir/_images/
119
+ const imagesDir = join(ctx.dataDir, '_images');
120
+ if (!existsSync(imagesDir))
121
+ mkdirSync(imagesDir, { recursive: true });
122
+ const filename = `${randomUUID().slice(0, 8)}.png`;
123
+ const filepath = join(imagesDir, filename);
124
+ writeFileSync(filepath, Buffer.from(imageBytes, 'base64'));
125
+ console.log(`[ImageGen] Saved: ${filepath}`);
126
+ res.json({ success: true, src: `/_images/${filename}` });
127
+ }
128
+ catch (err) {
129
+ const msg = err?.message || 'Image generation failed';
130
+ console.error('[ImageGen] Generation failed:', msg);
131
+ const friendly = /Unexpected token.*<!DOCTYPE/i.test(msg)
132
+ ? 'Image generation failed — your Gemini API key may be invalid or expired. Check your GEMINI_API_KEY.'
133
+ : msg;
134
+ res.status(500).json({ success: false, error: friendly });
135
+ }
136
+ });
137
+ },
138
+ contextMenuItems() {
139
+ return [
140
+ {
141
+ label: 'Generate image',
142
+ action: 'img:generate',
143
+ condition: 'always',
144
+ promptForInput: true,
145
+ },
146
+ ];
147
+ },
148
+ };
149
+ export default plugin;
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@openwriter/plugin-image-gen",
3
+ "version": "0.1.0",
4
+ "description": "Generate images with AI — right-click empty paragraphs",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch"
10
+ },
11
+ "dependencies": {
12
+ "@google/genai": "^1.42.0"
13
+ },
14
+ "devDependencies": {
15
+ "@types/express": "^5.0.0",
16
+ "typescript": "^5.6.0"
17
+ },
18
+ "openwriter": {
19
+ "displayName": "Image Generator",
20
+ "category": "image-generation"
21
+ },
22
+ "files": [
23
+ "dist/",
24
+ "package.json"
25
+ ]
26
+ }
@@ -0,0 +1,66 @@
1
+ import MarkdownIt from 'markdown-it';
2
+ export interface ServerModules {
3
+ tiptapToMarkdown: (doc: any, title: string, metadata?: Record<string, any>) => string;
4
+ getDocument: () => any;
5
+ getTitle: () => string;
6
+ getMetadata: () => Record<string, any>;
7
+ getActiveProfile: () => string;
8
+ getDataDir: () => string;
9
+ getDocId: () => string;
10
+ platformFetch: (path: string, options?: RequestInit) => Promise<Response>;
11
+ }
12
+ export declare function getServerModules(): Promise<ServerModules>;
13
+ export interface PluginConfigField {
14
+ type: 'string' | 'number' | 'boolean';
15
+ required?: boolean;
16
+ env?: string;
17
+ description?: string;
18
+ }
19
+ export interface PluginMcpTool {
20
+ name: string;
21
+ description: string;
22
+ inputSchema: Record<string, unknown>;
23
+ handler: (params: Record<string, unknown>) => Promise<unknown>;
24
+ }
25
+ export interface PluginRouteContext {
26
+ app: import('express').Router;
27
+ config: Record<string, string>;
28
+ dataDir: string;
29
+ }
30
+ export interface PluginSidebarMenuItem {
31
+ label: string;
32
+ action: string;
33
+ promptForFocus?: boolean;
34
+ }
35
+ export interface OpenWriterPlugin {
36
+ name: string;
37
+ version: string;
38
+ description?: string;
39
+ category?: 'writing' | 'social-media' | 'image-generation' | 'publishing' | 'productivity' | 'analytics';
40
+ configSchema?: Record<string, PluginConfigField>;
41
+ registerRoutes?(ctx: PluginRouteContext): void | Promise<void>;
42
+ mcpTools?(config: Record<string, string>): PluginMcpTool[];
43
+ sidebarMenuItems?(): PluginSidebarMenuItem[];
44
+ }
45
+ export declare const md: MarkdownIt;
46
+ /** Strip YAML frontmatter and TipTap empty markers from markdown output */
47
+ export declare function stripFrontmatter(markdown: string): string;
48
+ /** Scan HTML for /_images/ references, read local files, return base64 array for R2 upload */
49
+ export declare function extractLocalImages(html: string): Promise<Array<{
50
+ path: string;
51
+ data: string;
52
+ content_type: string;
53
+ }>>;
54
+ /** Convert current document's TipTap JSON to body HTML + plain text */
55
+ export declare function documentToEmail(): Promise<{
56
+ html: string;
57
+ text: string;
58
+ subject: string;
59
+ json: any;
60
+ }>;
61
+ /** Strip markdown syntax to produce clean plain text for email */
62
+ export declare function markdownToPlainText(markdown: string): string;
63
+ /** Strip inline markdown marks from a string */
64
+ export declare function stripInline(text: string): string;
65
+ /** Make an authenticated request to the Publish API via platform proxy */
66
+ export declare function publishFetch(_config: Record<string, string>, path: string, options?: RequestInit): Promise<Response>;
@@ -0,0 +1,199 @@
1
+ import MarkdownIt from 'markdown-it';
2
+ import markdownItIns from 'markdown-it-ins';
3
+ import markdownItMark from 'markdown-it-mark';
4
+ import markdownItSub from 'markdown-it-sub';
5
+ import markdownItSup from 'markdown-it-sup';
6
+ import { readFileSync, existsSync } from 'fs';
7
+ import { join, extname } from 'path';
8
+ // Lazy-load server modules at runtime
9
+ // npm package: dist/plugins/publish/dist/helpers.js → ../../../server/
10
+ // Monorepo: plugins/publish/dist/helpers.js → ../../../packages/openwriter/dist/server/
11
+ const npmBase = new URL('../../../server/', import.meta.url).href;
12
+ const monoBase = new URL('../../../packages/openwriter/dist/server/', import.meta.url).href;
13
+ let _cached = null;
14
+ async function tryImport(base) {
15
+ const [markdown, state, helpers, connections] = await Promise.all([
16
+ import(base + 'markdown.js'),
17
+ import(base + 'state.js'),
18
+ import(base + 'helpers.js'),
19
+ import(base + 'connections.js'),
20
+ ]);
21
+ return { markdown, state, helpers, connections };
22
+ }
23
+ export async function getServerModules() {
24
+ if (_cached)
25
+ return _cached;
26
+ // Try npm package layout first, fall back to monorepo layout
27
+ let markdown, state, helpers, connections;
28
+ try {
29
+ ({ markdown, state, helpers, connections } = await tryImport(npmBase));
30
+ }
31
+ catch {
32
+ ({ markdown, state, helpers, connections } = await tryImport(monoBase));
33
+ }
34
+ _cached = {
35
+ tiptapToMarkdown: markdown.tiptapToMarkdown,
36
+ getDocument: state.getDocument,
37
+ getTitle: state.getTitle,
38
+ getMetadata: state.getMetadata,
39
+ getActiveProfile: helpers.getActiveProfile,
40
+ getDataDir: helpers.getDataDir,
41
+ getDocId: state.getDocId,
42
+ platformFetch: connections.platformFetch,
43
+ };
44
+ return _cached;
45
+ }
46
+ // markdown-it instance matching export-routes.ts configuration
47
+ export const md = new MarkdownIt({ linkify: false, html: true });
48
+ md.enable('strikethrough');
49
+ md.use(markdownItIns);
50
+ md.use(markdownItMark);
51
+ md.use(markdownItSub);
52
+ md.use(markdownItSup);
53
+ /** Strip YAML frontmatter and TipTap empty markers from markdown output */
54
+ export function stripFrontmatter(markdown) {
55
+ let result = markdown;
56
+ const fmMatch = result.match(/^---\n[\s\S]*?\n---\n\n/);
57
+ if (fmMatch)
58
+ result = result.slice(fmMatch[0].length);
59
+ result = result.replace(/^\s*<!--\s*-->\s*$/gm, '');
60
+ return result.trim();
61
+ }
62
+ /** Scan HTML for /_images/ references, read local files, return base64 array for R2 upload */
63
+ export async function extractLocalImages(html) {
64
+ const server = await getServerModules();
65
+ const dataDir = server.getDataDir();
66
+ const images = [];
67
+ const regex = /\/_images\/[^\s"'<>]+/g;
68
+ const seen = new Set();
69
+ let match;
70
+ while ((match = regex.exec(html)) !== null) {
71
+ const imgPath = match[0];
72
+ if (seen.has(imgPath))
73
+ continue;
74
+ seen.add(imgPath);
75
+ const localFile = join(dataDir, imgPath);
76
+ if (!existsSync(localFile))
77
+ continue;
78
+ const data = readFileSync(localFile).toString('base64');
79
+ const ext = extname(imgPath).toLowerCase();
80
+ const mimeMap = {
81
+ '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
82
+ '.png': 'image/png', '.gif': 'image/gif',
83
+ '.webp': 'image/webp', '.svg': 'image/svg+xml',
84
+ };
85
+ images.push({ path: imgPath, data, content_type: mimeMap[ext] || 'image/png' });
86
+ }
87
+ return images;
88
+ }
89
+ /** Convert current document's TipTap JSON to body HTML + plain text */
90
+ export async function documentToEmail() {
91
+ const server = await getServerModules();
92
+ const doc = server.getDocument();
93
+ const title = server.getTitle();
94
+ const metadata = server.getMetadata();
95
+ const raw = server.tiptapToMarkdown(doc, title, metadata);
96
+ const clean = stripFrontmatter(raw).trim();
97
+ const html = md.render(clean);
98
+ const text = markdownToPlainText(clean);
99
+ return { html, text, subject: title, json: doc };
100
+ }
101
+ /** Strip markdown syntax to produce clean plain text for email */
102
+ export function markdownToPlainText(markdown) {
103
+ const lines = markdown.split('\n');
104
+ const out = [];
105
+ let inCodeBlock = false;
106
+ let codeLines = [];
107
+ for (const line of lines) {
108
+ if (/^```/.test(line)) {
109
+ if (inCodeBlock) {
110
+ for (const cl of codeLines)
111
+ out.push(' ' + cl);
112
+ codeLines = [];
113
+ inCodeBlock = false;
114
+ }
115
+ else {
116
+ inCodeBlock = true;
117
+ }
118
+ continue;
119
+ }
120
+ if (inCodeBlock) {
121
+ codeLines.push(line);
122
+ continue;
123
+ }
124
+ if (/^\s*<!--.*-->\s*$/.test(line))
125
+ continue;
126
+ if (/^!\[.*\]\(.*\)\s*$/.test(line))
127
+ continue;
128
+ if (/^\|[\s:|-]+\|\s*$/.test(line))
129
+ continue;
130
+ if (/^\|(.+)\|\s*$/.test(line)) {
131
+ const cells = line
132
+ .slice(1, -1)
133
+ .split('|')
134
+ .map((c) => stripInline(c.trim()));
135
+ out.push(cells.join(' | '));
136
+ continue;
137
+ }
138
+ if (/^[-*_]{3,}\s*$/.test(line)) {
139
+ out.push('---');
140
+ continue;
141
+ }
142
+ const headerMatch = line.match(/^(#{1,6})\s+(.*)/);
143
+ if (headerMatch) {
144
+ out.push(stripInline(headerMatch[2]));
145
+ continue;
146
+ }
147
+ const bqMatch = line.match(/^(>\s?)+(.*)$/);
148
+ if (bqMatch) {
149
+ out.push(stripInline(bqMatch[2]));
150
+ continue;
151
+ }
152
+ const taskMatch = line.match(/^(\s*)[-*+]\s+\[([ xX])\]\s+(.*)/);
153
+ if (taskMatch) {
154
+ const indent = taskMatch[1];
155
+ const check = taskMatch[2] === ' ' ? '[ ]' : '[x]';
156
+ out.push(indent + check + ' ' + stripInline(taskMatch[3]));
157
+ continue;
158
+ }
159
+ const ulMatch = line.match(/^(\s*)[-*+]\s+(.*)/);
160
+ if (ulMatch) {
161
+ out.push(ulMatch[1] + '- ' + stripInline(ulMatch[2]));
162
+ continue;
163
+ }
164
+ const olMatch = line.match(/^(\s*)(\d+)\.\s+(.*)/);
165
+ if (olMatch) {
166
+ out.push(olMatch[1] + olMatch[2] + '. ' + stripInline(olMatch[3]));
167
+ continue;
168
+ }
169
+ out.push(stripInline(line));
170
+ }
171
+ if (inCodeBlock) {
172
+ for (const cl of codeLines)
173
+ out.push(' ' + cl);
174
+ }
175
+ return out
176
+ .join('\n')
177
+ .replace(/\n{3,}/g, '\n\n')
178
+ .trim();
179
+ }
180
+ /** Strip inline markdown marks from a string */
181
+ export function stripInline(text) {
182
+ return text
183
+ .replace(/!\[([^\]]*)\]\([^)]+\)/g, '')
184
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
185
+ .replace(/`([^`]+)`/g, '$1')
186
+ .replace(/(\*{3}|_{3})(.+?)\1/g, '$2')
187
+ .replace(/(\*{2}|_{2})(.+?)\1/g, '$2')
188
+ .replace(/(\*|_)(.+?)\1/g, '$2')
189
+ .replace(/~~(.+?)~~/g, '$1')
190
+ .replace(/\+\+(.+?)\+\+/g, '$1')
191
+ .replace(/==(.+?)==/g, '$1')
192
+ .replace(/~([^~]+)~/g, '$1')
193
+ .replace(/\^([^^]+)\^/g, '$1');
194
+ }
195
+ /** Make an authenticated request to the Publish API via platform proxy */
196
+ export async function publishFetch(_config, path, options = {}) {
197
+ const server = await getServerModules();
198
+ return server.platformFetch(path, options);
199
+ }
@@ -0,0 +1,3 @@
1
+ import type { OpenWriterPlugin } from './helpers.js';
2
+ declare const plugin: OpenWriterPlugin;
3
+ export default plugin;