openwriter 0.27.0 → 0.28.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-3no79ry9.css +1 -0
- package/dist/client/assets/{index-DgUPw-v5.js → index-DzHT4klX.js} +54 -54
- package/dist/client/index.html +2 -2
- package/dist/plugins/authors-voice/dist/index.d.ts +5 -9
- package/dist/plugins/authors-voice/dist/index.js +17 -130
- package/dist/server/autoplug-enroll.js +71 -0
- package/dist/server/index.js +20 -2
- package/dist/server/scheduler-routes.js +29 -1
- package/package.json +1 -1
- package/dist/client/assets/index-BJMpYpj1.css +0 -1
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-DzHT4klX.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-3no79ry9.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
|
|
4
|
-
* for
|
|
5
|
-
*
|
|
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
|
|
4
|
-
* for
|
|
5
|
-
*
|
|
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.
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual-post → autoplug enrollment bridge.
|
|
3
|
+
*
|
|
4
|
+
* API/scheduler posts enter the platform's autoplug tracking at post time
|
|
5
|
+
* (the platform calls autoTrackTweet on /connections/:id/post, post-thread,
|
|
6
|
+
* the scheduler cron, and on POST /publications). Manual posts only ever
|
|
7
|
+
* wrote local `tweetContext.lastPost` frontmatter and never told the platform,
|
|
8
|
+
* so they never enrolled — and quote tweets, which the X API cannot post and
|
|
9
|
+
* therefore ALWAYS go through manual mark-sent, could never be autoplug-tracked.
|
|
10
|
+
*
|
|
11
|
+
* This reconciles the two: when a tweet/article doc is marked posted with a
|
|
12
|
+
* tweet URL, we record a publication on the platform. The platform's
|
|
13
|
+
* POST /publications handler enrolls X publications into autoplug tracking,
|
|
14
|
+
* so engagement-threshold autoplugs now fire on manually-posted tweets the
|
|
15
|
+
* same way they do for API/scheduler posts.
|
|
16
|
+
*/
|
|
17
|
+
import { isAuthenticated, platformFetch } from './connections.js';
|
|
18
|
+
const TWEET_ID_RE = /(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/i;
|
|
19
|
+
/** Pull the numeric tweet id out of an x.com / twitter.com status URL. */
|
|
20
|
+
export function extractTweetId(url) {
|
|
21
|
+
if (!url)
|
|
22
|
+
return null;
|
|
23
|
+
const m = TWEET_ID_RE.exec(url);
|
|
24
|
+
return m ? m[1] : null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Enroll a manually-posted tweet into platform autoplug tracking.
|
|
28
|
+
*
|
|
29
|
+
* Best-effort and idempotent: skips when the tweet is already recorded as a
|
|
30
|
+
* publication for this doc, so re-marking, unmark/remark, and autosave churn
|
|
31
|
+
* never duplicate the enrollment. Never throws — enrollment must never block
|
|
32
|
+
* or break the mark-sent metadata write.
|
|
33
|
+
*/
|
|
34
|
+
export async function enrollManualPostForAutoplug(docId, tweetUrl, text) {
|
|
35
|
+
if (!isAuthenticated())
|
|
36
|
+
return;
|
|
37
|
+
const tweetId = extractTweetId(tweetUrl);
|
|
38
|
+
if (!tweetId)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
// Idempotency: the platform's publications table has no unique constraint,
|
|
42
|
+
// so dedupe here before recording another row + re-enrolling.
|
|
43
|
+
const existingRes = await platformFetch(`/publications?documentId=${encodeURIComponent(docId)}`);
|
|
44
|
+
if (existingRes.ok) {
|
|
45
|
+
const data = await existingRes.json();
|
|
46
|
+
const already = (data.publications || []).some((p) => p.platform === 'x' && p.external_id === tweetId);
|
|
47
|
+
if (already)
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Recording the publication triggers autoTrackTweet platform-side, which
|
|
51
|
+
// enrolls the tweet for autoplug rule evaluation (guarded there by the
|
|
52
|
+
// profile having ≥1 enabled goal + an active X connection).
|
|
53
|
+
const res = await platformFetch('/publications', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
document_id: docId,
|
|
57
|
+
platform: 'x',
|
|
58
|
+
external_id: tweetId,
|
|
59
|
+
url: tweetUrl,
|
|
60
|
+
meta: { text: text || '', last_tweet_id: tweetId },
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
if (res.ok) {
|
|
64
|
+
console.log(`[Autoplug] Enrolled manual post ${tweetId} (doc ${docId}) into tracking`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Best-effort: platform may be unreachable / unauthenticated. Mark-sent
|
|
69
|
+
// already succeeded locally; tracking simply won't enroll this time.
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -11,8 +11,9 @@ import { setupWebSocket, broadcastAgentStatus, broadcastDocumentSwitched, broadc
|
|
|
11
11
|
import { TOOL_REGISTRY } from './mcp.js';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
14
|
-
import { save, cancelDebouncedSave, load, getDocument, getTitle, getFilePath, getDocId, getMetadata, getStatus, updateDocument, setMetadata, applyTextEdits, isAgentLocked, getPendingDocInfo, getDocTagsByFilename, addDocTag, removeDocTag, markAllNodesAsPending, updatePendingCacheForActiveDoc, removePendingCacheEntry, clearAllCaches, stripPendingAttrs, stripPendingAttrsFromFile, setAutoAcceptOnFile, setSortRequestOnFile, clearSortRequestOnFile, bumpDocVersion, markAsAgentStub } from './state.js';
|
|
14
|
+
import { save, cancelDebouncedSave, load, getDocument, getTitle, getFilePath, getDocId, getMetadata, getStatus, updateDocument, setMetadata, applyTextEdits, isAgentLocked, getPendingDocInfo, getDocTagsByFilename, addDocTag, removeDocTag, markAllNodesAsPending, updatePendingCacheForActiveDoc, removePendingCacheEntry, clearAllCaches, stripPendingAttrs, stripPendingAttrsFromFile, setAutoAcceptOnFile, setSortRequestOnFile, clearSortRequestOnFile, bumpDocVersion, markAsAgentStub, extractText } from './state.js';
|
|
15
15
|
import { syncPostHistory } from './post-sync.js';
|
|
16
|
+
import { enrollManualPostForAutoplug } from './autoplug-enroll.js';
|
|
16
17
|
import { listDocuments, switchDocument, createDocument, deleteDocument, duplicateDocument, reloadDocument, updateDocumentTitle, openFile, reorderDocs, searchDocuments, listArchivedDocuments, archiveDocument, unarchiveDocument, getActiveFilename, batchResolve, listPendingSorts } from './documents.js';
|
|
17
18
|
import { createWorkspaceRouter } from './workspace-routes.js';
|
|
18
19
|
import { createLinkRouter } from './link-routes.js';
|
|
@@ -162,10 +163,27 @@ export async function startHttpServer(options = {}) {
|
|
|
162
163
|
// Update document metadata from browser (e.g. view toggle in Appearance panel)
|
|
163
164
|
app.post('/api/metadata', (req, res) => {
|
|
164
165
|
try {
|
|
165
|
-
|
|
166
|
+
const body = req.body || {};
|
|
167
|
+
setMetadata(body);
|
|
166
168
|
save();
|
|
167
169
|
broadcastMetadataChanged(getMetadata());
|
|
168
170
|
broadcastDocumentsChanged();
|
|
171
|
+
// Reconcile manual mark-sent with autoplug tracking. When this write
|
|
172
|
+
// marks a tweet/article posted with a tweet URL, enroll the tweet so
|
|
173
|
+
// engagement autoplugs fire on manual posts — including quote tweets,
|
|
174
|
+
// which the X API can't post and so always reach here, never the API
|
|
175
|
+
// path that already enrolls. Capture docId/text synchronously (this is
|
|
176
|
+
// the active doc) then fire-and-forget; enrollment never blocks the save.
|
|
177
|
+
// Honors the per-doc opt-out: `autoplug: false` skips enrollment entirely
|
|
178
|
+
// (the manual lane just makes no platform call).
|
|
179
|
+
const tweetUrl = body?.tweetContext?.lastPost?.tweetUrl || body?.articleContext?.lastPost?.tweetUrl;
|
|
180
|
+
if (tweetUrl && getMetadata()?.autoplug !== false) {
|
|
181
|
+
const docId = getDocId();
|
|
182
|
+
if (docId) {
|
|
183
|
+
const text = extractText(getDocument()?.content || []);
|
|
184
|
+
void enrollManualPostForAutoplug(docId, tweetUrl, text);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
169
187
|
res.json({ success: true });
|
|
170
188
|
}
|
|
171
189
|
catch (err) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { Router } from 'express';
|
|
5
5
|
import { platformFetch, isAuthenticated } from './connections.js';
|
|
6
6
|
import { syncPostHistory } from './post-sync.js';
|
|
7
|
+
import { getMetadata } from './state.js';
|
|
7
8
|
export function createSchedulerRouter() {
|
|
8
9
|
const router = Router();
|
|
9
10
|
function proxy(path, method = 'GET') {
|
|
@@ -74,7 +75,34 @@ export function createSchedulerRouter() {
|
|
|
74
75
|
});
|
|
75
76
|
// Queue
|
|
76
77
|
router.get('/api/scheduler/queue', proxy('/scheduler/queue'));
|
|
77
|
-
router.post('/api/scheduler/queue',
|
|
78
|
+
router.post('/api/scheduler/queue', async (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
if (!isAuthenticated()) {
|
|
81
|
+
res.json({ error: 'Not authenticated' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const body = { ...req.body };
|
|
85
|
+
// Honor the active doc's autoplug opt-out: fold no_autoplug into the
|
|
86
|
+
// content JSONB (normalizing a bare-string content to { text }) so the
|
|
87
|
+
// platform cron skips enrollment for this scheduled post.
|
|
88
|
+
if (getMetadata()?.autoplug === false) {
|
|
89
|
+
const c = body.content;
|
|
90
|
+
body.content = (c && typeof c === 'object')
|
|
91
|
+
? { ...c, no_autoplug: true }
|
|
92
|
+
: { text: typeof c === 'string' ? c : '', no_autoplug: true };
|
|
93
|
+
}
|
|
94
|
+
const upstream = await platformFetch('/scheduler/queue', { method: 'POST', body: JSON.stringify(body) });
|
|
95
|
+
const data = await upstream.json();
|
|
96
|
+
if (!upstream.ok) {
|
|
97
|
+
res.status(upstream.status).json(data);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
res.json(data);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
res.status(500).json({ error: err.message });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
78
106
|
router.patch('/api/scheduler/queue/:id', async (req, res) => {
|
|
79
107
|
try {
|
|
80
108
|
if (!isAuthenticated()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openwriter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|