openwriter 0.26.0 → 0.27.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.
@@ -10,8 +10,8 @@
10
10
  <link rel="preconnect" href="https://fonts.googleapis.com" />
11
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
12
  <link href="https://fonts.googleapis.com/css2?family=Charter:ital,wght@0,400;0,700;1,400&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;0,700;1,400&family=DM+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=DM+Serif+Display&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;500;600;700&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Literata:ital,opsz,wght@0,7..72,400;0,7..72,600;0,7..72,700;1,7..72,400&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,600;1,6..72,400&family=Playfair+Display:wght@400;600;700;900&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;0,8..60,700;1,8..60,400&family=Space+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" />
13
- <script type="module" crossorigin src="/assets/index-DmHLFNTs.js"></script>
14
- <link rel="stylesheet" crossorigin href="/assets/index-AWIKUHJ_.css">
13
+ <script type="module" crossorigin src="/assets/index-DgUPw-v5.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/assets/index-BJMpYpj1.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Author's Voice plugin for OpenWriter.
3
- * Proxies /api/voice/* to the AV backend and adds context menu items
4
- * for rewriting, shrinking, expanding, and custom instructions.
5
- * Also registers sidebar menu items for document-level transforms.
3
+ * Proxies /api/voice/* to the AV backend and adds editor context-menu items
4
+ * for sub-paragraph text actions (Enhance / Modify / Shrink / Expand / Insert / Fill).
5
+ *
6
+ * Sidebar document transforms (Vary / Shrinkify / Threadify / etc.) live in
7
+ * @openwriter/plugin-publish — they go through the metered platform path.
6
8
  */
7
9
  import type { Express } from 'express';
8
10
  interface PluginConfigField {
@@ -22,11 +24,6 @@ interface PluginContextMenuItem {
22
24
  condition?: 'has-selection' | 'empty-node' | 'always';
23
25
  promptForInput?: boolean;
24
26
  }
25
- interface PluginSidebarMenuItem {
26
- label: string;
27
- action: string;
28
- promptForFocus?: boolean;
29
- }
30
27
  interface OpenWriterPlugin {
31
28
  name: string;
32
29
  version: string;
@@ -35,7 +32,6 @@ interface OpenWriterPlugin {
35
32
  configSchema?: Record<string, PluginConfigField>;
36
33
  registerRoutes?(ctx: PluginRouteContext): void | Promise<void>;
37
34
  contextMenuItems?(): PluginContextMenuItem[];
38
- sidebarMenuItems?(): PluginSidebarMenuItem[];
39
35
  }
40
36
  declare const plugin: OpenWriterPlugin;
41
37
  export default plugin;
@@ -1,32 +1,14 @@
1
1
  /**
2
2
  * Author's Voice plugin for OpenWriter.
3
- * Proxies /api/voice/* to the AV backend and adds context menu items
4
- * for rewriting, shrinking, expanding, and custom instructions.
5
- * Also registers sidebar menu items for document-level transforms.
3
+ * Proxies /api/voice/* to the AV backend and adds editor context-menu items
4
+ * for sub-paragraph text actions (Enhance / Modify / Shrink / Expand / Insert / Fill).
5
+ *
6
+ * Sidebar document transforms (Vary / Shrinkify / Threadify / etc.) live in
7
+ * @openwriter/plugin-publish — they go through the metered platform path.
6
8
  */
7
- /** Simple HTML → markdown conversion for document creation */
8
- function htmlToMarkdown(html) {
9
- let md = html;
10
- // <hr> → horizontal rule
11
- md = md.replace(/<hr\s*\/?>/gi, '\n---\n');
12
- // <br> → newline
13
- md = md.replace(/<br\s*\/?>/gi, '\n');
14
- // <strong>/<b> → **bold**
15
- md = md.replace(/<(strong|b)>([\s\S]*?)<\/\1>/gi, '**$2**');
16
- // <em>/<i> → *italic*
17
- md = md.replace(/<(em|i)>([\s\S]*?)<\/\1>/gi, '*$2*');
18
- // <p> → paragraph boundaries
19
- md = md.replace(/<p[^>]*>/gi, '');
20
- md = md.replace(/<\/p>/gi, '\n\n');
21
- // Strip remaining tags
22
- md = md.replace(/<[^>]+>/g, '');
23
- // Normalize whitespace
24
- md = md.replace(/\n{3,}/g, '\n\n');
25
- return md.trim();
26
- }
27
9
  const plugin = {
28
10
  name: '@openwriter/plugin-authors-voice',
29
- version: '0.1.0',
11
+ version: '0.4.0',
30
12
  description: "Rewrite text in your voice using Author's Voice",
31
13
  category: 'writing',
32
14
  configSchema: {
@@ -45,113 +27,31 @@ const plugin = {
45
27
  registerRoutes(ctx) {
46
28
  const backendUrl = ctx.config['backend-url'] || process.env.AV_BACKEND_URL || 'https://authors-voice.com';
47
29
  const apiKey = ctx.config['api-key'] || process.env.AV_API_KEY || '';
30
+ const debugEnabled = process.env.AV_DEBUG === '1' || process.env.AV_DEBUG === 'true';
48
31
  const authHeaders = () => {
49
32
  const h = { 'Content-Type': 'application/json' };
50
33
  if (apiKey)
51
34
  h['Authorization'] = `Bearer ${apiKey}`;
52
35
  return h;
53
36
  };
54
- // Sidebar action handler must be registered BEFORE the wildcard
55
- ctx.app.post('/api/voice/sidebar-action', async (req, res) => {
56
- try {
57
- const { action, filename, title, instructions, content } = req.body;
58
- console.log(`[AV Plugin] Sidebar action: ${action} on "${title}"`);
59
- if (!content) {
60
- res.status(400).json({ error: 'Document content is required' });
61
- return;
62
- }
63
- // Call AV backend transform endpoint
64
- const transformUrl = `${backendUrl}/api/voice/transform`;
65
- const upstream = await fetch(transformUrl, {
66
- method: 'POST',
67
- headers: authHeaders(),
68
- body: JSON.stringify({ action, content, title, instructions }),
69
- });
70
- if (!upstream.ok) {
71
- const errData = await upstream.json().catch(() => ({}));
72
- console.error('[AV Plugin] Transform failed:', upstream.status, errData);
73
- res.status(upstream.status).json(errData);
74
- return;
75
- }
76
- const transformResult = await upstream.json();
77
- // Convert HTML output to markdown for document creation
78
- let markdownContent = htmlToMarkdown(transformResult.html);
79
- // Threadify: always create as tweet template
80
- const createBody = {
81
- title: transformResult.newTitle,
82
- content: markdownContent,
83
- markPending: true,
84
- agentCreated: true,
85
- };
86
- if (action === 'threadify') {
87
- // Build TipTap JSON directly to avoid markdown parsing issues.
88
- // Markdown parser converts "- item" lines to bulletList nodes that the
89
- // tweet editor can't render (bulletList extension is disabled), causing
90
- // empty gaps. By building JSON with only paragraph + hardBreak nodes,
91
- // all tweet text stays as plain text.
92
- if (transformResult.thread?.tweets?.length) {
93
- const docContent = [];
94
- transformResult.thread.tweets.forEach((t, i) => {
95
- // Single paragraph per tweet. Split on \n only:
96
- // \n → one hardBreak (tight line), \n\n → two hardBreaks (blank line spacing)
97
- const lines = t.text.split('\n');
98
- const nodes = [];
99
- lines.forEach((line, j) => {
100
- if (j > 0)
101
- nodes.push({ type: 'hardBreak' });
102
- if (line)
103
- nodes.push({ type: 'text', text: line });
104
- });
105
- if (nodes.length) {
106
- docContent.push({ type: 'paragraph', content: nodes });
107
- }
108
- if (i < transformResult.thread.tweets.length - 1) {
109
- docContent.push({ type: 'horizontalRule' });
110
- }
111
- });
112
- createBody.content = { type: 'doc', content: docContent };
113
- }
114
- createBody.metadata = { tweetContext: { mode: 'tweet' } };
115
- }
116
- // Create new document in OpenWriter via internal HTTP call
117
- const host = req.get('host') || 'localhost:5050';
118
- const protocol = req.protocol || 'http';
119
- const createUrl = `${protocol}://${host}/api/documents`;
120
- const createRes = await fetch(createUrl, {
121
- method: 'POST',
122
- headers: { 'Content-Type': 'application/json' },
123
- body: JSON.stringify(createBody),
124
- });
125
- if (!createRes.ok) {
126
- const errData = await createRes.json().catch(() => ({}));
127
- console.error('[AV Plugin] Document creation failed:', errData);
128
- res.status(500).json({ error: 'Failed to create result document' });
129
- return;
130
- }
131
- const docResult = await createRes.json();
132
- res.json({
133
- success: true,
134
- action,
135
- filename: docResult.filename,
136
- title: transformResult.newTitle,
137
- metadata: transformResult.metadata,
138
- });
139
- }
140
- catch (err) {
141
- console.error('[AV Plugin] Sidebar action error:', err?.message || err);
142
- res.status(500).json({ error: 'Sidebar action failed' });
143
- }
144
- });
145
- // Wildcard proxy for all other /api/voice/* routes
37
+ const withDebug = (body) => {
38
+ if (!debugEnabled || !body || typeof body !== 'object')
39
+ return body;
40
+ return { ...body, debug: true };
41
+ };
42
+ // Wildcard proxy for /api/voice/* routes. Pure pass-through: the AV API owns the
43
+ // engine choice (v1/v2) via its own AV_DEFAULT_ENGINE setting, so the plugin injects
44
+ // nothing but the optional owner-only dev debug flag.
146
45
  ctx.app.post('/api/voice/*', async (req, res) => {
147
46
  try {
148
47
  const subPath = req.params[0] || '';
149
48
  const targetUrl = `${backendUrl}/api/voice/${subPath}`;
49
+ const body = withDebug(req.body);
150
50
  console.log(`[AV Plugin] ${req.method} ${req.path} → ${targetUrl}`);
151
51
  const upstream = await fetch(targetUrl, {
152
52
  method: 'POST',
153
53
  headers: authHeaders(),
154
- body: JSON.stringify(req.body),
54
+ body: JSON.stringify(body),
155
55
  });
156
56
  res.status(upstream.status);
157
57
  const forwardHeaders = ['x-usage-rewrite-count', 'x-usage-rewrite-limit', 'x-usage-resets-at'];
@@ -189,18 +89,5 @@ const plugin = {
189
89
  { label: 'Fill sentence', action: 'av:fill-sentence', condition: 'empty-node' },
190
90
  ];
191
91
  },
192
- // Sidebar transforms disabled — now handled by publish plugin.
193
- // Kept commented for reference during transition.
194
- // sidebarMenuItems() {
195
- // return [
196
- // { label: 'Vary', action: 'voice:vary', promptForFocus: true },
197
- // { label: 'Shrinkify', action: 'voice:shrinkify', promptForFocus: true },
198
- // { label: 'Expandify', action: 'voice:expandify', promptForFocus: true },
199
- // { label: 'Threadify', action: 'voice:threadify', promptForFocus: true },
200
- // { label: 'Storify', action: 'voice:storify', promptForFocus: true },
201
- // { label: 'Emailify', action: 'voice:emailify', promptForFocus: true },
202
- // { label: 'Postify', action: 'voice:postify', promptForFocus: true },
203
- // ];
204
- // },
205
92
  };
206
93
  export default plugin;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwriter/plugin-authors-voice",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
4
4
  "description": "Rewrite text in your voice using Author's Voice",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Blog publishing MCP tools — local git ops, no Worker, no PAT.
3
+ *
4
+ * Auth: piggybacks on the user's existing `gh auth login`.
5
+ * Persistence: blogSites in plugins['@openwriter/plugin-github'].blogSites.
6
+ */
7
+ import { type PluginMcpTool } from './helpers.js';
8
+ export declare function blogTools(): PluginMcpTool[];