openwriter 0.6.3 → 0.6.4
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-C0ddsTTl.css +1 -0
- package/dist/client/assets/index-IGiG5fjk.js +209 -0
- package/dist/client/index.html +2 -2
- package/dist/plugins/authors-voice/dist/index.d.ts +41 -0
- package/dist/plugins/authors-voice/dist/index.js +203 -0
- package/dist/plugins/authors-voice/package.json +23 -0
- package/dist/plugins/image-gen/dist/index.d.ts +35 -0
- package/dist/plugins/image-gen/dist/index.js +88 -0
- package/dist/plugins/image-gen/package.json +26 -0
- package/dist/plugins/publish/dist/helpers.d.ts +54 -0
- package/dist/plugins/publish/dist/helpers.js +185 -0
- package/dist/plugins/publish/dist/index.d.ts +3 -0
- package/dist/plugins/publish/dist/index.js +697 -0
- package/dist/plugins/publish/dist/newsletter-tools.d.ts +2 -0
- package/dist/plugins/publish/dist/newsletter-tools.js +364 -0
- package/dist/plugins/publish/package.json +31 -0
- package/dist/plugins/x-api/dist/index.d.ts +27 -0
- package/dist/plugins/x-api/dist/index.js +217 -0
- package/dist/plugins/x-api/package.json +26 -0
- package/dist/server/documents.js +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/plugin-discovery.js +35 -8
- package/dist/server/plugin-manager.js +2 -2
- package/dist/server/scheduler-routes.js +191 -0
- package/package.json +3 -2
- package/skill/SKILL.md +3 -2
- package/dist/client/assets/index-cxT2LD1Q.js +0 -209
- package/dist/client/assets/index-rHyhyRQQ.css +0 -1
package/dist/server/documents.js
CHANGED
|
@@ -100,7 +100,7 @@ export function listDocuments() {
|
|
|
100
100
|
wordCount,
|
|
101
101
|
isActive: fullPath === currentPath,
|
|
102
102
|
...(data.docId ? { docId: data.docId } : {}),
|
|
103
|
-
...(data.newsletterContext?.lastSend?.sentAt ? { lastSent: data.newsletterContext.lastSend.sentAt } : data.tweetContext?.lastPost?.postedAt ? { lastSent: data.tweetContext.lastPost.postedAt } : data.blogContext?.lastPublish?.publishedAt ? { lastSent: data.blogContext.lastPublish.publishedAt } : {}),
|
|
103
|
+
...(data.newsletterContext?.lastSend?.sentAt ? { lastSent: data.newsletterContext.lastSend.sentAt } : data.tweetContext?.lastPost?.postedAt ? { lastSent: data.tweetContext.lastPost.postedAt } : data.blogContext?.lastPublish?.publishedAt ? { lastSent: data.blogContext.lastPublish.publishedAt } : data.articleContext?.lastPost?.postedAt ? { lastSent: data.articleContext.lastPost.postedAt } : data.manualPost?.postedAt ? { lastSent: data.manualPost.postedAt } : {}),
|
|
104
104
|
...(data.tweetContext?.lastPost?.tweetUrl ? { postedUrl: data.tweetContext.lastPost.tweetUrl } : {}),
|
|
105
105
|
};
|
|
106
106
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -12,19 +12,25 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
12
12
|
const __dirname = dirname(__filename);
|
|
13
13
|
const USER_PLUGINS_DIR = join(homedir(), '.openwriter', 'plugins');
|
|
14
14
|
/**
|
|
15
|
-
* Scan
|
|
16
|
-
*
|
|
15
|
+
* Scan for bundled plugins. Checks two locations:
|
|
16
|
+
* 1. dist/plugins/ — plugins copied into the npm package at publish time
|
|
17
|
+
* 2. monorepo root plugins/ — local dev with workspace symlinks
|
|
17
18
|
*/
|
|
18
19
|
function discoverBundledPlugins() {
|
|
19
|
-
//
|
|
20
|
-
const
|
|
20
|
+
// 1. Monorepo root: dist/server/ → ../../../../plugins/ (live dev code)
|
|
21
|
+
const monoPluginsDir = join(__dirname, '..', '..', '..', '..', 'plugins');
|
|
22
|
+
// 2. Bundled in dist: dist/server/ → ../plugins/ (npm install)
|
|
23
|
+
const distPluginsDir = join(__dirname, '..', 'plugins');
|
|
24
|
+
// Prefer monorepo path in dev (live code), fall back to bundled copy in npm install
|
|
25
|
+
const pluginsDir = existsSync(monoPluginsDir) ? monoPluginsDir : distPluginsDir;
|
|
21
26
|
if (!existsSync(pluginsDir))
|
|
22
27
|
return [];
|
|
23
28
|
const results = [];
|
|
24
29
|
for (const entry of readdirSync(pluginsDir, { withFileTypes: true })) {
|
|
25
30
|
if (!entry.isDirectory())
|
|
26
31
|
continue;
|
|
27
|
-
const
|
|
32
|
+
const pluginDir = join(pluginsDir, entry.name);
|
|
33
|
+
const pkgPath = join(pluginDir, 'package.json');
|
|
28
34
|
if (!existsSync(pkgPath))
|
|
29
35
|
continue;
|
|
30
36
|
try {
|
|
@@ -40,6 +46,7 @@ function discoverBundledPlugins() {
|
|
|
40
46
|
source: 'bundled',
|
|
41
47
|
displayName: manifest?.displayName,
|
|
42
48
|
category: manifest?.category,
|
|
49
|
+
pluginDir,
|
|
43
50
|
});
|
|
44
51
|
}
|
|
45
52
|
catch {
|
|
@@ -101,12 +108,20 @@ function tryAddPlugin(pkgDir, fullName, results) {
|
|
|
101
108
|
source: 'user',
|
|
102
109
|
displayName: manifest?.displayName,
|
|
103
110
|
category: manifest?.category,
|
|
111
|
+
pluginDir: pkgDir,
|
|
104
112
|
});
|
|
105
113
|
}
|
|
106
114
|
catch {
|
|
107
115
|
// Skip malformed package.json
|
|
108
116
|
}
|
|
109
117
|
}
|
|
118
|
+
/** Preferred display order for bundled plugins */
|
|
119
|
+
const BUNDLED_ORDER = [
|
|
120
|
+
'@openwriter/plugin-authors-voice',
|
|
121
|
+
'@openwriter/plugin-publish',
|
|
122
|
+
'@openwriter/plugin-image-gen',
|
|
123
|
+
'@openwriter/plugin-x-api',
|
|
124
|
+
];
|
|
110
125
|
/**
|
|
111
126
|
* Discover all plugins from both bundled and user sources.
|
|
112
127
|
* Deduplicates by name (bundled takes priority).
|
|
@@ -114,6 +129,12 @@ function tryAddPlugin(pkgDir, fullName, results) {
|
|
|
114
129
|
export function discoverPlugins() {
|
|
115
130
|
const bundled = discoverBundledPlugins();
|
|
116
131
|
const user = discoverUserPlugins();
|
|
132
|
+
// Sort bundled plugins by preferred order (unknown plugins go to end)
|
|
133
|
+
bundled.sort((a, b) => {
|
|
134
|
+
const ai = BUNDLED_ORDER.indexOf(a.name);
|
|
135
|
+
const bi = BUNDLED_ORDER.indexOf(b.name);
|
|
136
|
+
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
|
137
|
+
});
|
|
117
138
|
// Deduplicate: bundled wins if same name exists in both
|
|
118
139
|
const seen = new Set(bundled.map(p => p.name));
|
|
119
140
|
const deduped = [...bundled];
|
|
@@ -126,10 +147,10 @@ export function discoverPlugins() {
|
|
|
126
147
|
return deduped;
|
|
127
148
|
}
|
|
128
149
|
/**
|
|
129
|
-
* Import a plugin
|
|
130
|
-
*
|
|
150
|
+
* Import a plugin module and extract its metadata.
|
|
151
|
+
* Uses pluginDir for path-based imports (works in both npm install and monorepo dev).
|
|
131
152
|
*/
|
|
132
|
-
export async function loadPluginModule(name, source = 'bundled') {
|
|
153
|
+
export async function loadPluginModule(name, source = 'bundled', pluginDir) {
|
|
133
154
|
try {
|
|
134
155
|
let mod;
|
|
135
156
|
if (source === 'user') {
|
|
@@ -138,7 +159,13 @@ export async function loadPluginModule(name, source = 'bundled') {
|
|
|
138
159
|
const resolved = userRequire.resolve(name);
|
|
139
160
|
mod = await import(pathToFileURL(resolved).href);
|
|
140
161
|
}
|
|
162
|
+
else if (pluginDir) {
|
|
163
|
+
// Path-based import — works for both bundled-in-dist and monorepo plugins
|
|
164
|
+
const mainPath = join(pluginDir, 'dist', 'index.js');
|
|
165
|
+
mod = await import(pathToFileURL(mainPath).href);
|
|
166
|
+
}
|
|
141
167
|
else {
|
|
168
|
+
// Fallback: bare import (workspace symlinks in monorepo dev)
|
|
142
169
|
mod = await import(name);
|
|
143
170
|
}
|
|
144
171
|
const plugin = mod.default || mod.plugin || mod;
|
|
@@ -20,7 +20,7 @@ export class PluginManager {
|
|
|
20
20
|
const savedPlugins = savedConfig.plugins || {};
|
|
21
21
|
for (const d of discovered) {
|
|
22
22
|
// Load module to get configSchema
|
|
23
|
-
const loaded = await loadPluginModule(d.name, d.source);
|
|
23
|
+
const loaded = await loadPluginModule(d.name, d.source, d.pluginDir);
|
|
24
24
|
const saved = savedPlugins[d.name];
|
|
25
25
|
this.plugins.set(d.name, {
|
|
26
26
|
discovered: d,
|
|
@@ -41,7 +41,7 @@ export class PluginManager {
|
|
|
41
41
|
return { success: true };
|
|
42
42
|
// Ensure plugin module is loaded
|
|
43
43
|
if (!managed.plugin) {
|
|
44
|
-
const loaded = await loadPluginModule(name, managed.discovered.source);
|
|
44
|
+
const loaded = await loadPluginModule(name, managed.discovered.source, managed.discovered.pluginDir);
|
|
45
45
|
if (!loaded)
|
|
46
46
|
return { success: false, error: `Failed to import "${name}"` };
|
|
47
47
|
managed.plugin = loaded.plugin;
|
|
@@ -117,5 +117,196 @@ export function createSchedulerRouter() {
|
|
|
117
117
|
router.get('/api/scheduler/history', proxy('/scheduler/history'));
|
|
118
118
|
// Available connections for scheduler
|
|
119
119
|
router.get('/api/scheduler/connections', proxy('/scheduler/connections'));
|
|
120
|
+
// --- Autoplugs ---
|
|
121
|
+
// Goals
|
|
122
|
+
router.get('/api/scheduler/autoplugs/goals', proxy('/scheduler/autoplugs/goals'));
|
|
123
|
+
router.post('/api/scheduler/autoplugs/goals', proxy('/scheduler/autoplugs/goals', 'POST'));
|
|
124
|
+
router.patch('/api/scheduler/autoplugs/goals/:id', async (req, res) => {
|
|
125
|
+
try {
|
|
126
|
+
if (!isAuthenticated()) {
|
|
127
|
+
res.json({ error: 'Not authenticated' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const upstream = await platformFetch(`/scheduler/autoplugs/goals/${req.params.id}`, {
|
|
131
|
+
method: 'PATCH',
|
|
132
|
+
body: JSON.stringify(req.body),
|
|
133
|
+
});
|
|
134
|
+
const data = await upstream.json();
|
|
135
|
+
if (!upstream.ok) {
|
|
136
|
+
res.status(upstream.status).json(data);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
res.json(data);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
res.status(500).json({ error: err.message });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
router.delete('/api/scheduler/autoplugs/goals/:id', async (req, res) => {
|
|
146
|
+
try {
|
|
147
|
+
if (!isAuthenticated()) {
|
|
148
|
+
res.json({ error: 'Not authenticated' });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const upstream = await platformFetch(`/scheduler/autoplugs/goals/${req.params.id}`, { method: 'DELETE' });
|
|
152
|
+
const data = await upstream.json();
|
|
153
|
+
if (!upstream.ok) {
|
|
154
|
+
res.status(upstream.status).json(data);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
res.json(data);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
res.status(500).json({ error: err.message });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
// Pool (per goal)
|
|
164
|
+
router.get('/api/scheduler/autoplugs/goals/:id/pool', async (req, res) => {
|
|
165
|
+
try {
|
|
166
|
+
if (!isAuthenticated()) {
|
|
167
|
+
res.json({ error: 'Not authenticated' });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const upstream = await platformFetch(`/scheduler/autoplugs/goals/${req.params.id}/pool`);
|
|
171
|
+
const data = await upstream.json();
|
|
172
|
+
if (!upstream.ok) {
|
|
173
|
+
res.status(upstream.status).json(data);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
res.json(data);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
res.status(500).json({ error: err.message });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
router.post('/api/scheduler/autoplugs/goals/:id/pool', async (req, res) => {
|
|
183
|
+
try {
|
|
184
|
+
if (!isAuthenticated()) {
|
|
185
|
+
res.json({ error: 'Not authenticated' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const upstream = await platformFetch(`/scheduler/autoplugs/goals/${req.params.id}/pool`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
body: JSON.stringify(req.body),
|
|
191
|
+
});
|
|
192
|
+
const data = await upstream.json();
|
|
193
|
+
if (!upstream.ok) {
|
|
194
|
+
res.status(upstream.status).json(data);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
res.json(data);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
res.status(500).json({ error: err.message });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
router.patch('/api/scheduler/autoplugs/pool/:id', async (req, res) => {
|
|
204
|
+
try {
|
|
205
|
+
if (!isAuthenticated()) {
|
|
206
|
+
res.json({ error: 'Not authenticated' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const upstream = await platformFetch(`/scheduler/autoplugs/pool/${req.params.id}`, {
|
|
210
|
+
method: 'PATCH',
|
|
211
|
+
body: JSON.stringify(req.body),
|
|
212
|
+
});
|
|
213
|
+
const data = await upstream.json();
|
|
214
|
+
if (!upstream.ok) {
|
|
215
|
+
res.status(upstream.status).json(data);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
res.json(data);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
res.status(500).json({ error: err.message });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
router.delete('/api/scheduler/autoplugs/pool/:id', async (req, res) => {
|
|
225
|
+
try {
|
|
226
|
+
if (!isAuthenticated()) {
|
|
227
|
+
res.json({ error: 'Not authenticated' });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const upstream = await platformFetch(`/scheduler/autoplugs/pool/${req.params.id}`, { method: 'DELETE' });
|
|
231
|
+
const data = await upstream.json();
|
|
232
|
+
if (!upstream.ok) {
|
|
233
|
+
res.status(upstream.status).json(data);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
res.json(data);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
res.status(500).json({ error: err.message });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
// Rules
|
|
243
|
+
router.get('/api/scheduler/autoplugs/rules', proxy('/scheduler/autoplugs/rules'));
|
|
244
|
+
router.post('/api/scheduler/autoplugs/rules', proxy('/scheduler/autoplugs/rules', 'POST'));
|
|
245
|
+
router.patch('/api/scheduler/autoplugs/rules/:id', async (req, res) => {
|
|
246
|
+
try {
|
|
247
|
+
if (!isAuthenticated()) {
|
|
248
|
+
res.json({ error: 'Not authenticated' });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const upstream = await platformFetch(`/scheduler/autoplugs/rules/${req.params.id}`, {
|
|
252
|
+
method: 'PATCH',
|
|
253
|
+
body: JSON.stringify(req.body),
|
|
254
|
+
});
|
|
255
|
+
const data = await upstream.json();
|
|
256
|
+
if (!upstream.ok) {
|
|
257
|
+
res.status(upstream.status).json(data);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
res.json(data);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
res.status(500).json({ error: err.message });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
router.delete('/api/scheduler/autoplugs/rules/:id', async (req, res) => {
|
|
267
|
+
try {
|
|
268
|
+
if (!isAuthenticated()) {
|
|
269
|
+
res.json({ error: 'Not authenticated' });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const upstream = await platformFetch(`/scheduler/autoplugs/rules/${req.params.id}`, { method: 'DELETE' });
|
|
273
|
+
const data = await upstream.json();
|
|
274
|
+
if (!upstream.ok) {
|
|
275
|
+
res.status(upstream.status).json(data);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
res.json(data);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
res.status(500).json({ error: err.message });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
// AV Key
|
|
285
|
+
router.post('/api/scheduler/autoplugs/av-key', proxy('/scheduler/autoplugs/av-key', 'POST'));
|
|
286
|
+
router.delete('/api/scheduler/autoplugs/av-key', proxy('/scheduler/autoplugs/av-key', 'DELETE'));
|
|
287
|
+
router.get('/api/scheduler/autoplugs/av-key/status', proxy('/scheduler/autoplugs/av-key/status'));
|
|
288
|
+
// Tracking
|
|
289
|
+
router.get('/api/scheduler/autoplugs/tracking', proxy('/scheduler/autoplugs/tracking'));
|
|
290
|
+
router.patch('/api/scheduler/autoplugs/tracking/:id', async (req, res) => {
|
|
291
|
+
try {
|
|
292
|
+
if (!isAuthenticated()) {
|
|
293
|
+
res.json({ error: 'Not authenticated' });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const upstream = await platformFetch(`/scheduler/autoplugs/tracking/${req.params.id}`, {
|
|
297
|
+
method: 'PATCH',
|
|
298
|
+
body: JSON.stringify(req.body),
|
|
299
|
+
});
|
|
300
|
+
const data = await upstream.json();
|
|
301
|
+
if (!upstream.ok) {
|
|
302
|
+
res.status(upstream.status).json(data);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
res.json(data);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
res.status(500).json({ error: err.message });
|
|
309
|
+
}
|
|
310
|
+
});
|
|
120
311
|
return router;
|
|
121
312
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openwriter",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "vite build && tsc -p tsconfig.server.json",
|
|
35
|
-
"prepublishOnly": "node
|
|
35
|
+
"prepublishOnly": "node scripts/prepublish.cjs",
|
|
36
36
|
"preview": "node dist/bin/pad.js",
|
|
37
37
|
"lint": "eslint src server bin --ext .ts,.tsx"
|
|
38
38
|
},
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"react-dom": "^18.3.1",
|
|
74
74
|
"trash": "^10.1.0",
|
|
75
75
|
"ws": "^8.18.0",
|
|
76
|
+
"@xdevplatform/xdk": "^0.4.0",
|
|
76
77
|
"zod": "^3.25.76"
|
|
77
78
|
},
|
|
78
79
|
"devDependencies": {
|
package/skill/SKILL.md
CHANGED
|
@@ -15,7 +15,7 @@ description: |
|
|
|
15
15
|
Requires: OpenWriter MCP server configured. Browser UI at localhost:5050.
|
|
16
16
|
metadata:
|
|
17
17
|
author: travsteward
|
|
18
|
-
version: "0.2.
|
|
18
|
+
version: "0.2.4"
|
|
19
19
|
repository: https://github.com/travsteward/openwriter
|
|
20
20
|
license: MIT
|
|
21
21
|
---
|
|
@@ -91,8 +91,9 @@ After setup, tell the user:
|
|
|
91
91
|
Every document has an immutable **docId** (8-char hex, e.g. `a1b2c3d4`) in its YAML frontmatter. Titles are for human communication and agent reasoning. DocIds are for agent action.
|
|
92
92
|
|
|
93
93
|
- `list_documents` and `read_pad` always show both title and docId
|
|
94
|
-
- All doc-targeting tools take `docId` as their parameter (not filename)
|
|
94
|
+
- All doc-targeting tools take `docId` as their parameter (not filename, not frontmatter read from disk)
|
|
95
95
|
- Two documents can have the same title — the docId disambiguates
|
|
96
|
+
- Filenames contain UUIDs unrelated to docIds — the first segment of a filename UUID looks like a docId but is not
|
|
96
97
|
|
|
97
98
|
## MCP Tools Reference (36 core + 21 publish platform)
|
|
98
99
|
|