openwriter 0.9.3 → 0.10.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/dist/client/assets/{index-ZQ1BICWp.css → index-CuPYxtxy.css} +1 -1
- package/dist/client/assets/index-deMuWDiP.js +211 -0
- package/dist/client/index.html +2 -2
- package/dist/plugins/authors-voice/dist/index.js +13 -11
- package/dist/plugins/publish/dist/helpers.d.ts +12 -0
- package/dist/plugins/publish/dist/index.js +138 -0
- package/dist/server/documents.js +6 -0
- package/dist/server/index.js +16 -1
- package/package.json +1 -1
- package/dist/client/assets/index-wRyjoTwK.js +0 -211
package/dist/client/index.html
CHANGED
|
@@ -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-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-deMuWDiP.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CuPYxtxy.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -189,16 +189,18 @@ const plugin = {
|
|
|
189
189
|
{ label: 'Fill sentence', action: 'av:fill-sentence', condition: 'empty-node' },
|
|
190
190
|
];
|
|
191
191
|
},
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
},
|
|
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
|
+
// },
|
|
203
205
|
};
|
|
204
206
|
export default plugin;
|
|
@@ -22,13 +22,25 @@ export interface PluginMcpTool {
|
|
|
22
22
|
inputSchema: Record<string, unknown>;
|
|
23
23
|
handler: (params: Record<string, unknown>) => Promise<unknown>;
|
|
24
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
|
+
}
|
|
25
35
|
export interface OpenWriterPlugin {
|
|
26
36
|
name: string;
|
|
27
37
|
version: string;
|
|
28
38
|
description?: string;
|
|
29
39
|
category?: 'writing' | 'social-media' | 'image-generation' | 'publishing' | 'productivity' | 'analytics';
|
|
30
40
|
configSchema?: Record<string, PluginConfigField>;
|
|
41
|
+
registerRoutes?(ctx: PluginRouteContext): void | Promise<void>;
|
|
31
42
|
mcpTools?(config: Record<string, string>): PluginMcpTool[];
|
|
43
|
+
sidebarMenuItems?(): PluginSidebarMenuItem[];
|
|
32
44
|
}
|
|
33
45
|
export declare const md: MarkdownIt;
|
|
34
46
|
/** Strip YAML frontmatter and TipTap empty markers from markdown output */
|
|
@@ -2,6 +2,41 @@ import { getServerModules, publishFetch } from './helpers.js';
|
|
|
2
2
|
import { newsletterTools } from './newsletter-tools.js';
|
|
3
3
|
import { readFileSync, existsSync } from 'fs';
|
|
4
4
|
import { join, extname } from 'path';
|
|
5
|
+
/** Extract docId from raw markdown frontmatter (JSON or YAML) */
|
|
6
|
+
function extractDocId(rawContent) {
|
|
7
|
+
// JSON frontmatter: "docId":"abc123"
|
|
8
|
+
const jsonMatch = rawContent.match(/"docId"\s*:\s*"([^"]+)"/);
|
|
9
|
+
if (jsonMatch)
|
|
10
|
+
return jsonMatch[1];
|
|
11
|
+
// YAML frontmatter: docId: abc123
|
|
12
|
+
const yamlMatch = rawContent.match(/^docId:\s*["']?(\S+?)["']?\s*$/m);
|
|
13
|
+
if (yamlMatch)
|
|
14
|
+
return yamlMatch[1];
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
/** Map transform action to variant content type */
|
|
18
|
+
const ACTION_VARIANT_TYPE = {
|
|
19
|
+
vary: 'document',
|
|
20
|
+
shrinkify: 'document',
|
|
21
|
+
expandify: 'document',
|
|
22
|
+
threadify: 'tweet',
|
|
23
|
+
storify: 'document',
|
|
24
|
+
emailify: 'newsletter',
|
|
25
|
+
postify: 'tweet',
|
|
26
|
+
};
|
|
27
|
+
/** Simple HTML → markdown conversion for document creation */
|
|
28
|
+
function htmlToMarkdown(html) {
|
|
29
|
+
let md = html;
|
|
30
|
+
md = md.replace(/<hr\s*\/?>/gi, '\n---\n');
|
|
31
|
+
md = md.replace(/<br\s*\/?>/gi, '\n');
|
|
32
|
+
md = md.replace(/<(strong|b)>([\s\S]*?)<\/\1>/gi, '**$2**');
|
|
33
|
+
md = md.replace(/<(em|i)>([\s\S]*?)<\/\1>/gi, '*$2*');
|
|
34
|
+
md = md.replace(/<p[^>]*>/gi, '');
|
|
35
|
+
md = md.replace(/<\/p>/gi, '\n\n');
|
|
36
|
+
md = md.replace(/<[^>]+>/g, '');
|
|
37
|
+
md = md.replace(/\n{3,}/g, '\n\n');
|
|
38
|
+
return md.trim();
|
|
39
|
+
}
|
|
5
40
|
const plugin = {
|
|
6
41
|
name: '@openwriter/plugin-publish',
|
|
7
42
|
version: '0.1.0',
|
|
@@ -988,5 +1023,108 @@ const plugin = {
|
|
|
988
1023
|
},
|
|
989
1024
|
];
|
|
990
1025
|
},
|
|
1026
|
+
registerRoutes(ctx) {
|
|
1027
|
+
// Sidebar action handler for document transforms
|
|
1028
|
+
ctx.app.post('/api/publish/sidebar-action', async (req, res) => {
|
|
1029
|
+
try {
|
|
1030
|
+
const { action, filename, title, instructions, content } = req.body;
|
|
1031
|
+
console.log(`[Publish Plugin] Sidebar action: ${action} on "${title}"`);
|
|
1032
|
+
if (!content) {
|
|
1033
|
+
res.status(400).json({ error: 'Document content is required' });
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
// Call publish worker /transforms endpoint
|
|
1037
|
+
const transformRes = await publishFetch(ctx.config, '/transforms', {
|
|
1038
|
+
method: 'POST',
|
|
1039
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1040
|
+
body: JSON.stringify({ action, content, title, instructions }),
|
|
1041
|
+
});
|
|
1042
|
+
if (!transformRes.ok) {
|
|
1043
|
+
const errData = await transformRes.json().catch(() => ({}));
|
|
1044
|
+
console.error('[Publish Plugin] Transform failed:', transformRes.status, errData);
|
|
1045
|
+
res.status(transformRes.status).json(errData);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const transformResult = await transformRes.json();
|
|
1049
|
+
// Convert HTML output to markdown for document creation
|
|
1050
|
+
let markdownContent = htmlToMarkdown(transformResult.html);
|
|
1051
|
+
// Extract source doc's docId for variant relationship
|
|
1052
|
+
const masterDocId = extractDocId(content);
|
|
1053
|
+
const variantType = ACTION_VARIANT_TYPE[action] || 'document';
|
|
1054
|
+
// Build document creation payload
|
|
1055
|
+
const createBody = {
|
|
1056
|
+
title: transformResult.newTitle,
|
|
1057
|
+
content: markdownContent,
|
|
1058
|
+
markPending: true,
|
|
1059
|
+
agentCreated: true,
|
|
1060
|
+
...(masterDocId ? { masterDocId, variantType } : {}),
|
|
1061
|
+
};
|
|
1062
|
+
if (action === 'threadify') {
|
|
1063
|
+
// Build TipTap JSON directly — markdown parser converts "- item" lines
|
|
1064
|
+
// to bulletList nodes the tweet editor can't render. Using paragraph +
|
|
1065
|
+
// hardBreak nodes keeps all tweet text as plain text.
|
|
1066
|
+
if (transformResult.thread?.tweets?.length) {
|
|
1067
|
+
const docContent = [];
|
|
1068
|
+
transformResult.thread.tweets.forEach((t, i) => {
|
|
1069
|
+
const lines = t.text.split('\n');
|
|
1070
|
+
const nodes = [];
|
|
1071
|
+
lines.forEach((line, j) => {
|
|
1072
|
+
if (j > 0)
|
|
1073
|
+
nodes.push({ type: 'hardBreak' });
|
|
1074
|
+
if (line)
|
|
1075
|
+
nodes.push({ type: 'text', text: line });
|
|
1076
|
+
});
|
|
1077
|
+
if (nodes.length) {
|
|
1078
|
+
docContent.push({ type: 'paragraph', content: nodes });
|
|
1079
|
+
}
|
|
1080
|
+
if (i < transformResult.thread.tweets.length - 1) {
|
|
1081
|
+
docContent.push({ type: 'horizontalRule' });
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
createBody.content = { type: 'doc', content: docContent };
|
|
1085
|
+
}
|
|
1086
|
+
createBody.metadata = { tweetContext: { mode: 'tweet' } };
|
|
1087
|
+
}
|
|
1088
|
+
// Create new document via internal HTTP call
|
|
1089
|
+
const host = req.get('host') || 'localhost:5050';
|
|
1090
|
+
const protocol = req.protocol || 'http';
|
|
1091
|
+
const createUrl = `${protocol}://${host}/api/documents`;
|
|
1092
|
+
const createRes = await fetch(createUrl, {
|
|
1093
|
+
method: 'POST',
|
|
1094
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1095
|
+
body: JSON.stringify(createBody),
|
|
1096
|
+
});
|
|
1097
|
+
if (!createRes.ok) {
|
|
1098
|
+
const errData = await createRes.json().catch(() => ({}));
|
|
1099
|
+
console.error('[Publish Plugin] Document creation failed:', errData);
|
|
1100
|
+
res.status(500).json({ error: 'Failed to create result document' });
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const docResult = await createRes.json();
|
|
1104
|
+
res.json({
|
|
1105
|
+
success: true,
|
|
1106
|
+
action,
|
|
1107
|
+
filename: docResult.filename,
|
|
1108
|
+
title: transformResult.newTitle,
|
|
1109
|
+
metadata: transformResult.metadata,
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
catch (err) {
|
|
1113
|
+
console.error('[Publish Plugin] Sidebar action error:', err?.message || err);
|
|
1114
|
+
res.status(500).json({ error: 'Sidebar action failed' });
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
},
|
|
1118
|
+
sidebarMenuItems() {
|
|
1119
|
+
return [
|
|
1120
|
+
{ label: 'Vary', action: 'publish:vary', promptForFocus: true },
|
|
1121
|
+
{ label: 'Shrinkify', action: 'publish:shrinkify', promptForFocus: true },
|
|
1122
|
+
{ label: 'Expandify', action: 'publish:expandify', promptForFocus: true },
|
|
1123
|
+
{ label: 'Threadify', action: 'publish:threadify', promptForFocus: true },
|
|
1124
|
+
{ label: 'Storify', action: 'publish:storify', promptForFocus: true },
|
|
1125
|
+
{ label: 'Emailify', action: 'publish:emailify', promptForFocus: true },
|
|
1126
|
+
{ label: 'Postify', action: 'publish:postify', promptForFocus: true },
|
|
1127
|
+
];
|
|
1128
|
+
},
|
|
991
1129
|
};
|
|
992
1130
|
export default plugin;
|
package/dist/server/documents.js
CHANGED
|
@@ -120,6 +120,8 @@ export function listDocuments() {
|
|
|
120
120
|
...(data.tweetContext?.lastPost?.tweetUrl ? { postedUrl: data.tweetContext.lastPost.tweetUrl } : {}),
|
|
121
121
|
...(data.newsletterContext ? { isNewsletter: true } : {}),
|
|
122
122
|
...(deriveContentType(data) ? { contentType: deriveContentType(data) } : {}),
|
|
123
|
+
...(data.masterDocId ? { masterDocId: data.masterDocId } : {}),
|
|
124
|
+
...(data.variantType ? { variantType: data.variantType } : {}),
|
|
123
125
|
};
|
|
124
126
|
}
|
|
125
127
|
catch {
|
|
@@ -158,6 +160,8 @@ export function listDocuments() {
|
|
|
158
160
|
...(data.tweetContext?.lastPost?.tweetUrl ? { postedUrl: data.tweetContext.lastPost.tweetUrl } : {}),
|
|
159
161
|
...(data.newsletterContext ? { isNewsletter: true } : {}),
|
|
160
162
|
...(deriveContentType(data) ? { contentType: deriveContentType(data) } : {}),
|
|
163
|
+
...(data.masterDocId ? { masterDocId: data.masterDocId } : {}),
|
|
164
|
+
...(data.variantType ? { variantType: data.variantType } : {}),
|
|
161
165
|
});
|
|
162
166
|
}
|
|
163
167
|
catch { /* skip unreadable external files */ }
|
|
@@ -217,6 +221,8 @@ export function listArchivedDocuments() {
|
|
|
217
221
|
wordCount,
|
|
218
222
|
isActive: false,
|
|
219
223
|
...(data.docId ? { docId: data.docId } : {}),
|
|
224
|
+
...(data.masterDocId ? { masterDocId: data.masterDocId } : {}),
|
|
225
|
+
...(data.variantType ? { variantType: data.variantType } : {}),
|
|
220
226
|
archivedAt: data.archivedAt,
|
|
221
227
|
};
|
|
222
228
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -230,6 +230,16 @@ export async function startHttpServer(options = {}) {
|
|
|
230
230
|
setMetadata(req.body.metadata);
|
|
231
231
|
save();
|
|
232
232
|
}
|
|
233
|
+
// Variant relationship — set masterDocId and variantType in frontmatter
|
|
234
|
+
if (req.body.masterDocId || req.body.variantType) {
|
|
235
|
+
const variantMeta = {};
|
|
236
|
+
if (req.body.masterDocId)
|
|
237
|
+
variantMeta.masterDocId = req.body.masterDocId;
|
|
238
|
+
if (req.body.variantType)
|
|
239
|
+
variantMeta.variantType = req.body.variantType;
|
|
240
|
+
setMetadata(variantMeta);
|
|
241
|
+
save();
|
|
242
|
+
}
|
|
233
243
|
// Plugin flags: mark all content as pending + tag as agent-created
|
|
234
244
|
if (req.body.markPending) {
|
|
235
245
|
markAllNodesAsPending(getDocument(), 'insert');
|
|
@@ -708,9 +718,14 @@ export async function startHttpServer(options = {}) {
|
|
|
708
718
|
}
|
|
709
719
|
}
|
|
710
720
|
catch { /* content stays empty */ }
|
|
721
|
+
// Extract source doc's docId for variant spinner positioning
|
|
722
|
+
let sourceDocId;
|
|
723
|
+
const docIdMatch = docContent.match(/"docId"\s*:\s*"([^"]+)"/);
|
|
724
|
+
if (docIdMatch)
|
|
725
|
+
sourceDocId = docIdMatch[1];
|
|
711
726
|
// Show sidebar spinner while plugin processes
|
|
712
727
|
const spinnerTitle = label ? `${label}: ${title}` : title;
|
|
713
|
-
broadcastWritingStarted(spinnerTitle);
|
|
728
|
+
broadcastWritingStarted(spinnerTitle, sourceDocId ? { wsFilename: '', containerId: null, parentDocId: sourceDocId } : undefined);
|
|
714
729
|
// Intercept res.json to clear spinner when plugin handler responds
|
|
715
730
|
const origJson = res.json.bind(res);
|
|
716
731
|
res.json = (body) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openwriter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The open-source writing surface for AI agents. Markdown-native editor with pending change review — your agent writes, you accept or reject.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|