openwriter 0.11.0 → 0.12.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.
@@ -1,23 +0,0 @@
1
- {
2
- "name": "@openwriter/plugin-authors-voice",
3
- "version": "0.1.0",
4
- "description": "Rewrite text in your voice using Author's Voice",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "scripts": {
8
- "build": "tsc",
9
- "dev": "tsc --watch"
10
- },
11
- "devDependencies": {
12
- "@types/express": "^5.0.0",
13
- "typescript": "^5.6.0"
14
- },
15
- "openwriter": {
16
- "displayName": "Author's Voice",
17
- "category": "writing"
18
- },
19
- "files": [
20
- "dist/",
21
- "package.json"
22
- ]
23
- }
@@ -1,35 +0,0 @@
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;
@@ -1,141 +0,0 @@
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, 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
- },
30
- body: JSON.stringify({ prompt, aspect_ratio: aspectRatio }),
31
- });
32
- if (!res.ok) {
33
- const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
34
- throw new Error(err.error || 'Platform image generation failed');
35
- }
36
- const data = await res.json();
37
- // Download image and save locally
38
- const imageRes = await fetch(data.url);
39
- const imageBuffer = Buffer.from(await imageRes.arrayBuffer());
40
- const imagesDir = join(dataDir, '_images');
41
- if (!existsSync(imagesDir))
42
- mkdirSync(imagesDir, { recursive: true });
43
- const filename = `${randomUUID().slice(0, 8)}.png`;
44
- writeFileSync(join(imagesDir, filename), imageBuffer);
45
- console.log(`[ImageGen] Saved (platform): ${join(imagesDir, filename)}`);
46
- return { success: true, src: `/_images/${filename}` };
47
- }
48
- const plugin = {
49
- name: '@openwriter/plugin-image-gen',
50
- version: '0.1.0',
51
- description: 'Generate images with AI — right-click empty paragraphs',
52
- category: 'image-generation',
53
- configSchema: {
54
- 'gemini-api-key': {
55
- type: 'string',
56
- env: 'GEMINI_API_KEY',
57
- required: false,
58
- description: 'Google Gemini API key for image generation (optional — falls back to publish platform)',
59
- },
60
- },
61
- registerRoutes(ctx) {
62
- ctx.app.post('/api/image-gen/generate', async (req, res) => {
63
- try {
64
- const { prompt } = req.body;
65
- if (!prompt || typeof prompt !== 'string') {
66
- res.status(400).json({ success: false, error: 'prompt is required' });
67
- return;
68
- }
69
- if (prompt.length > 1000) {
70
- res.status(400).json({ success: false, error: 'prompt must be under 1000 characters' });
71
- return;
72
- }
73
- const apiKey = ctx.config['gemini-api-key'] || process.env.GEMINI_API_KEY || '';
74
- let imageBytes;
75
- if (apiKey) {
76
- // Local Gemini generation
77
- const { GoogleGenAI } = await import('@google/genai');
78
- const ai = new GoogleGenAI({ apiKey });
79
- console.log(`[ImageGen] Generating image (local): "${prompt.slice(0, 80)}..."`);
80
- const response = await ai.models.generateContent({
81
- model: 'gemini-3.1-flash-image-preview',
82
- contents: `Generate a 16:9 aspect ratio image: ${prompt}`,
83
- config: {
84
- responseModalities: ['IMAGE'],
85
- },
86
- });
87
- const parts = response.candidates?.[0]?.content?.parts;
88
- if (!parts || parts.length === 0) {
89
- res.status(422).json({ success: false, error: 'No image generated — content may have been filtered' });
90
- return;
91
- }
92
- const imagePart = parts.find((p) => p.inlineData);
93
- imageBytes = imagePart?.inlineData?.data;
94
- if (!imageBytes) {
95
- res.status(422).json({ success: false, error: 'No image data in response' });
96
- return;
97
- }
98
- }
99
- else {
100
- // Fallback: generate via publish platform API
101
- const platformResult = await generateViaPlatform(prompt, ctx.dataDir);
102
- if (!platformResult) {
103
- 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.' });
104
- return;
105
- }
106
- // platformResult already saved to disk
107
- res.json(platformResult);
108
- return;
109
- }
110
- // Save to dataDir/_images/
111
- const imagesDir = join(ctx.dataDir, '_images');
112
- if (!existsSync(imagesDir))
113
- mkdirSync(imagesDir, { recursive: true });
114
- const filename = `${randomUUID().slice(0, 8)}.png`;
115
- const filepath = join(imagesDir, filename);
116
- writeFileSync(filepath, Buffer.from(imageBytes, 'base64'));
117
- console.log(`[ImageGen] Saved: ${filepath}`);
118
- res.json({ success: true, src: `/_images/${filename}` });
119
- }
120
- catch (err) {
121
- const msg = err?.message || 'Image generation failed';
122
- console.error('[ImageGen] Generation failed:', msg);
123
- const friendly = /Unexpected token.*<!DOCTYPE/i.test(msg)
124
- ? 'Image generation failed — your Gemini API key may be invalid or expired. Check your GEMINI_API_KEY.'
125
- : msg;
126
- res.status(500).json({ success: false, error: friendly });
127
- }
128
- });
129
- },
130
- contextMenuItems() {
131
- return [
132
- {
133
- label: 'Generate image',
134
- action: 'img:generate',
135
- condition: 'always',
136
- promptForInput: true,
137
- },
138
- ];
139
- },
140
- };
141
- export default plugin;
@@ -1,26 +0,0 @@
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
- }
@@ -1,66 +0,0 @@
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>;
@@ -1,199 +0,0 @@
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
- }
@@ -1,3 +0,0 @@
1
- import type { OpenWriterPlugin } from './helpers.js';
2
- declare const plugin: OpenWriterPlugin;
3
- export default plugin;