openwriter 0.10.0 → 0.12.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.
Files changed (40) hide show
  1. package/dist/bin/pad.js +146 -101
  2. package/dist/client/assets/index-CNmzNvB_.js +211 -0
  3. package/dist/client/assets/index-CRImKlcp.css +1 -0
  4. package/dist/client/index.html +2 -2
  5. package/dist/server/documents.js +2 -0
  6. package/dist/server/index.js +46 -6
  7. package/dist/server/markdown-parse.js +11 -0
  8. package/dist/server/markdown-serialize.js +4 -1
  9. package/dist/server/mcp.js +89 -13
  10. package/dist/server/state.js +98 -29
  11. package/dist/server/ws.js +68 -15
  12. package/package.json +1 -1
  13. package/skill/SKILL.md +43 -1
  14. package/skill/docs/anti-ai.md +71 -0
  15. package/skill/docs/voices.md +88 -0
  16. package/skill/voices/authority.md +102 -0
  17. package/skill/voices/business.md +103 -0
  18. package/skill/voices/logical.md +104 -0
  19. package/skill/voices/provocateur.md +101 -0
  20. package/skill/voices/storyteller.md +104 -0
  21. package/dist/client/assets/index-CuPYxtxy.css +0 -1
  22. package/dist/client/assets/index-deMuWDiP.js +0 -211
  23. package/dist/plugins/authors-voice/dist/index.d.ts +0 -41
  24. package/dist/plugins/authors-voice/dist/index.js +0 -206
  25. package/dist/plugins/authors-voice/package.json +0 -23
  26. package/dist/plugins/image-gen/dist/index.d.ts +0 -35
  27. package/dist/plugins/image-gen/dist/index.js +0 -141
  28. package/dist/plugins/image-gen/package.json +0 -26
  29. package/dist/plugins/publish/dist/helpers.d.ts +0 -66
  30. package/dist/plugins/publish/dist/helpers.js +0 -199
  31. package/dist/plugins/publish/dist/index.d.ts +0 -3
  32. package/dist/plugins/publish/dist/index.js +0 -1130
  33. package/dist/plugins/publish/dist/newsletter-tools.d.ts +0 -2
  34. package/dist/plugins/publish/dist/newsletter-tools.js +0 -394
  35. package/dist/plugins/publish/package.json +0 -31
  36. package/dist/plugins/x-api/dist/index.d.ts +0 -27
  37. package/dist/plugins/x-api/dist/index.js +0 -240
  38. package/dist/plugins/x-api/package.json +0 -27
  39. package/dist/server/prompt-debug.js +0 -58
  40. package/dist/server/workspace-tags.js +0 -30
@@ -1,240 +0,0 @@
1
- /**
2
- * X API plugin for OpenWriter.
3
- * Registers routes for checking X connection status and posting tweets.
4
- * Uses @xdevplatform/xdk with OAuth1 credentials from plugin config.
5
- */
6
- import { Client, OAuth1 } from '@xdevplatform/xdk';
7
- import { join, extname } from 'path';
8
- import { readFileSync, existsSync } from 'fs';
9
- import sharp from 'sharp';
10
- import twitter from 'twitter-text';
11
- const { parseTweet } = twitter;
12
- function createXClient(config) {
13
- const apiKey = config['api-key'] || process.env.X_API_KEY || '';
14
- const apiSecret = config['api-secret'] || process.env.X_API_SECRET || '';
15
- const accessToken = config['access-token'] || process.env.X_ACCESS_TOKEN || '';
16
- const accessTokenSecret = config['access-token-secret'] || process.env.X_ACCESS_TOKEN_SECRET || '';
17
- if (!apiKey || !apiSecret || !accessToken || !accessTokenSecret)
18
- return null;
19
- const oauth1 = new OAuth1({
20
- apiKey,
21
- apiSecret,
22
- callback: 'oob',
23
- accessToken,
24
- accessTokenSecret,
25
- });
26
- return new Client({ oauth1 });
27
- }
28
- const plugin = {
29
- name: '@openwriter/plugin-x-api',
30
- version: '0.1.0',
31
- description: 'Post tweets from OpenWriter',
32
- category: 'social-media',
33
- configSchema: {
34
- 'api-key': { type: 'string', env: 'X_API_KEY', description: 'X API Key' },
35
- 'api-secret': { type: 'string', env: 'X_API_SECRET', description: 'X API Secret' },
36
- 'access-token': { type: 'string', env: 'X_ACCESS_TOKEN', description: 'X Access Token' },
37
- 'access-token-secret': { type: 'string', env: 'X_ACCESS_TOKEN_SECRET', description: 'X Access Token Secret' },
38
- },
39
- registerRoutes(ctx) {
40
- // GET /api/x/status — check if plugin is configured + authenticated
41
- ctx.app.get('/api/x/status', async (_req, res) => {
42
- try {
43
- const client = createXClient(ctx.config);
44
- if (!client) {
45
- res.json({ connected: false });
46
- return;
47
- }
48
- const me = await client.users.getMe();
49
- const username = me?.data?.username;
50
- res.json({ connected: true, username: username || undefined });
51
- }
52
- catch (err) {
53
- console.error('[X Plugin] Status check failed:', err.message);
54
- res.json({ connected: false, error: err.message });
55
- }
56
- });
57
- // POST /api/x/post — post a tweet (with optional media)
58
- ctx.app.post('/api/x/post', async (req, res) => {
59
- try {
60
- const { text, replyTo, quoteTweetId, mediaIds } = req.body;
61
- if ((!text || typeof text !== 'string') && (!Array.isArray(mediaIds) || mediaIds.length === 0)) {
62
- res.status(400).json({ success: false, error: 'text or mediaIds is required' });
63
- return;
64
- }
65
- if (mediaIds && (!Array.isArray(mediaIds) || mediaIds.length > 4)) {
66
- res.status(400).json({ success: false, error: 'mediaIds must be an array of 1-4 IDs' });
67
- return;
68
- }
69
- const client = createXClient(ctx.config);
70
- if (!client) {
71
- res.status(400).json({ success: false, error: 'X API credentials not configured' });
72
- return;
73
- }
74
- const body = {};
75
- if (text)
76
- body.text = text;
77
- if (replyTo) {
78
- body.reply = { inReplyToTweetId: replyTo };
79
- }
80
- if (quoteTweetId) {
81
- body.quoteTweetId = quoteTweetId;
82
- }
83
- if (mediaIds && mediaIds.length > 0) {
84
- body.media = { media_ids: mediaIds };
85
- }
86
- const result = await client.posts.create(body);
87
- const tweetId = result?.data?.id;
88
- const tweetUrl = tweetId ? `https://x.com/i/status/${tweetId}` : undefined;
89
- res.json({ success: true, tweetId, tweetUrl });
90
- }
91
- catch (err) {
92
- const detail = err.data ? JSON.stringify(err.data) : err.message;
93
- console.error('[X Plugin] Post failed:', detail);
94
- res.status(500).json({ success: false, error: detail });
95
- }
96
- });
97
- // POST /api/x/post-thread — post a full thread as a reply chain (with optional media per tweet)
98
- ctx.app.post('/api/x/post-thread', async (req, res) => {
99
- try {
100
- const { tweets, replyTo } = req.body;
101
- if (!Array.isArray(tweets) || tweets.length === 0) {
102
- res.status(400).json({ success: false, error: 'tweets must be a non-empty array' });
103
- return;
104
- }
105
- // Normalize: accept string[] or { text, mediaIds? }[]
106
- const normalized = tweets.map((t) => typeof t === 'string' ? { text: t, mediaIds: undefined } : t);
107
- // Validate character limits using X's weighted counting (emojis=2, URLs=23, CJK=2)
108
- const CHAR_LIMIT = 25000;
109
- const overLimit = normalized.map((t, i) => ({ i, len: parseTweet(t.text).weightedLength })).filter(x => x.len > CHAR_LIMIT);
110
- if (overLimit.length > 0) {
111
- res.status(400).json({
112
- success: false,
113
- error: `${overLimit.length} tweet(s) exceed ${CHAR_LIMIT} chars: ${overLimit.map(x => `#${x.i + 1} (${x.len})`).join(', ')}`,
114
- });
115
- return;
116
- }
117
- // Validate mediaIds per tweet
118
- for (let i = 0; i < normalized.length; i++) {
119
- const ids = normalized[i].mediaIds;
120
- if (ids && (!Array.isArray(ids) || ids.length > 4)) {
121
- res.status(400).json({ success: false, error: `Tweet ${i + 1}: mediaIds must be an array of 1-4 IDs` });
122
- return;
123
- }
124
- }
125
- const client = createXClient(ctx.config);
126
- if (!client) {
127
- res.status(400).json({ success: false, error: 'X API credentials not configured' });
128
- return;
129
- }
130
- const postedTweets = [];
131
- let previousTweetId = replyTo;
132
- for (let i = 0; i < normalized.length; i++) {
133
- const { text, mediaIds } = normalized[i];
134
- const body = {};
135
- if (text)
136
- body.text = text;
137
- if (previousTweetId) {
138
- body.reply = { inReplyToTweetId: previousTweetId };
139
- }
140
- if (mediaIds && mediaIds.length > 0) {
141
- body.media = { media_ids: mediaIds };
142
- }
143
- const result = await client.posts.create(body);
144
- const tweetId = result?.data?.id;
145
- if (!tweetId) {
146
- res.status(500).json({
147
- success: false,
148
- postedTweets,
149
- failedAt: i,
150
- error: `Tweet ${i + 1} posted but no ID returned`,
151
- });
152
- return;
153
- }
154
- postedTweets.push({ index: i, tweetId, text });
155
- previousTweetId = tweetId;
156
- }
157
- // Build thread URL from first tweet
158
- const firstTweetId = postedTweets[0]?.tweetId;
159
- const threadUrl = firstTweetId ? `https://x.com/i/status/${firstTweetId}` : undefined;
160
- console.log(`[X Plugin] Thread posted: ${postedTweets.length} tweets, ${threadUrl}`);
161
- res.json({ success: true, postedTweets, threadUrl });
162
- }
163
- catch (err) {
164
- console.error('[X Plugin] Post thread failed:', err.message);
165
- res.status(500).json({ success: false, error: err.message });
166
- }
167
- });
168
- // POST /api/x/upload-media — upload a local /_images/ file for tweet attachment
169
- ctx.app.post('/api/x/upload-media', async (req, res) => {
170
- try {
171
- const { src } = req.body;
172
- if (!src || typeof src !== 'string') {
173
- res.status(400).json({ success: false, error: 'src is required' });
174
- return;
175
- }
176
- // Security: only allow /_images/ paths, no traversal
177
- if (!/^\/_images\/[^/\\]+$/.test(src)) {
178
- res.status(400).json({ success: false, error: 'Invalid image path — must be /_images/<filename>' });
179
- return;
180
- }
181
- const filename = src.replace('/_images/', '');
182
- const filePath = join(ctx.dataDir, '_images', filename);
183
- if (!existsSync(filePath)) {
184
- res.status(404).json({ success: false, error: `Image not found: ${filename}` });
185
- return;
186
- }
187
- const ext = extname(filename).toLowerCase();
188
- const mimeMap = {
189
- '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
190
- '.png': 'image/png', '.webp': 'image/webp',
191
- '.gif': 'image/jpeg', '.bmp': 'image/bmp',
192
- '.tiff': 'image/tiff', '.tif': 'image/tiff',
193
- };
194
- const mediaType = mimeMap[ext] || 'image/jpeg';
195
- const client = createXClient(ctx.config);
196
- if (!client) {
197
- res.status(400).json({ success: false, error: 'X API credentials not configured' });
198
- return;
199
- }
200
- let fileBuffer = readFileSync(filePath);
201
- let uploadType = mediaType;
202
- const origSize = fileBuffer.length;
203
- // Compress large images or PNGs to JPEG to stay under X API limits
204
- if (fileBuffer.length > 3 * 1024 * 1024 || ext === '.png') {
205
- fileBuffer = Buffer.from(await sharp(fileBuffer).jpeg({ quality: 85 }).toBuffer());
206
- uploadType = 'image/jpeg';
207
- console.log(`[X Plugin] Compressed ${filename}: ${(origSize / 1024 / 1024).toFixed(2)}MB → ${(fileBuffer.length / 1024 / 1024).toFixed(2)}MB`);
208
- }
209
- console.log(`[X Plugin] Uploading ${filename}: ${(fileBuffer.length / 1024 / 1024).toFixed(2)}MB, type: ${uploadType}`);
210
- const mediaBase64 = fileBuffer.toString('base64');
211
- const uploadResult = await client.media.upload({
212
- body: { media: mediaBase64, mediaCategory: 'tweet_image', mediaType: uploadType },
213
- });
214
- const mediaId = uploadResult?.data?.id
215
- || uploadResult?.media_id_string;
216
- if (!mediaId) {
217
- res.status(500).json({ success: false, error: 'Upload succeeded but no media ID returned' });
218
- return;
219
- }
220
- console.log(`[X Plugin] Media uploaded: ${filename} → ${mediaId}`);
221
- res.json({ success: true, mediaId });
222
- }
223
- catch (err) {
224
- console.error('[X Plugin] Media upload failed:', err.message);
225
- if (err.response) {
226
- try {
227
- const body = await err.response.text();
228
- console.error('[X Plugin] X API response:', err.response.status, body);
229
- }
230
- catch { /* ignore */ }
231
- }
232
- if (err.data)
233
- console.error('[X Plugin] Error data:', JSON.stringify(err.data));
234
- console.error('[X Plugin] Full error:', JSON.stringify(err, Object.getOwnPropertyNames(err)));
235
- res.status(500).json({ success: false, error: err.message });
236
- }
237
- });
238
- },
239
- };
240
- export default plugin;
@@ -1,27 +0,0 @@
1
- {
2
- "name": "@openwriter/plugin-x-api",
3
- "version": "0.1.0",
4
- "description": "Post tweets from OpenWriter",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "scripts": {
8
- "build": "tsc",
9
- "dev": "tsc --watch"
10
- },
11
- "dependencies": {
12
- "@xdevplatform/xdk": "^0.4.0",
13
- "twitter-text": "^3.1.0"
14
- },
15
- "devDependencies": {
16
- "@types/express": "^5.0.0",
17
- "typescript": "^5.6.0"
18
- },
19
- "openwriter": {
20
- "displayName": "X / Twitter",
21
- "category": "social-media"
22
- },
23
- "files": [
24
- "dist/",
25
- "package.json"
26
- ]
27
- }
@@ -1,58 +0,0 @@
1
- /**
2
- * File: prompt-debug.ts
3
- * Purpose: Write AV prompt debug data to timestamped .md files for inspection.
4
- * Each enhance creates a new file in DATA_DIR, visible in the sidebar.
5
- */
6
- import { DATA_DIR, ensureDataDir, atomicWriteFileSync } from './helpers.js';
7
- import { join } from 'path';
8
- /**
9
- * Write prompt debug info to a timestamped markdown file.
10
- * Returns the filename created.
11
- */
12
- export function writePromptDebug(action, debug, metadata) {
13
- ensureDataDir();
14
- const now = new Date();
15
- const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
16
- const filename = `_prompt-${action || 'debug'}-${ts}.md`;
17
- const filePath = join(DATA_DIR, filename);
18
- const timeStr = now.toLocaleTimeString('en-US', { hour12: true, hour: '2-digit', minute: '2-digit', second: '2-digit' });
19
- let md = `---\ntitle: "Prompt Debug: ${action} @ ${timeStr}"\n---\n\n`;
20
- // Metadata summary
21
- if (metadata) {
22
- md += `## Metadata\n\n`;
23
- md += `| Key | Value |\n|-----|-------|\n`;
24
- if (metadata.action)
25
- md += `| Action | ${metadata.action} |\n`;
26
- if (metadata.profileUsed)
27
- md += `| Profile | ${metadata.profileUsed} |\n`;
28
- if (metadata.nodesIn != null)
29
- md += `| Nodes In | ${metadata.nodesIn} |\n`;
30
- if (metadata.nodesOut != null)
31
- md += `| Nodes Out | ${metadata.nodesOut} |\n`;
32
- if (metadata.ragExamples != null)
33
- md += `| RAG Examples | ${metadata.ragExamples} |\n`;
34
- if (metadata.ragTotalWords != null)
35
- md += `| RAG Total Words | ${metadata.ragTotalWords} |\n`;
36
- if (metadata.processingTimeMs != null)
37
- md += `| Processing Time | ${metadata.processingTimeMs}ms |\n`;
38
- if (metadata.estimatedCost != null)
39
- md += `| Estimated Cost | $${metadata.estimatedCost.toFixed(4)} |\n`;
40
- md += `\n`;
41
- }
42
- // System prompt
43
- if (debug.systemPrompt) {
44
- md += `## System Prompt\n\n`;
45
- md += debug.systemPrompt + '\n\n';
46
- }
47
- // User prompt
48
- if (debug.userPrompt) {
49
- md += `---\n\n## User Prompt\n\n`;
50
- md += debug.userPrompt + '\n\n';
51
- }
52
- // Raw LLM response (when available)
53
- if (debug.rawResponse) {
54
- md += `---\n\n## Raw LLM Output\n\n\`\`\`json\n${debug.rawResponse}\n\`\`\`\n\n`;
55
- }
56
- atomicWriteFileSync(filePath, md);
57
- return filename;
58
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * Tag index operations for workspace v2.
3
- * Tags are a cross-cutting index: tag → [file1, file2, ...].
4
- */
5
- export function addTag(tags, tagName, file) {
6
- if (!tags[tagName])
7
- tags[tagName] = [];
8
- if (!tags[tagName].includes(file))
9
- tags[tagName].push(file);
10
- }
11
- export function removeTag(tags, tagName, file) {
12
- if (!tags[tagName])
13
- return;
14
- tags[tagName] = tags[tagName].filter((f) => f !== file);
15
- if (tags[tagName].length === 0)
16
- delete tags[tagName];
17
- }
18
- export function removeFileFromAllTags(tags, file) {
19
- for (const tagName of Object.keys(tags)) {
20
- removeTag(tags, tagName, file);
21
- }
22
- }
23
- export function listTagsForFile(tags, file) {
24
- const result = [];
25
- for (const [tagName, files] of Object.entries(tags)) {
26
- if (files.includes(file))
27
- result.push(tagName);
28
- }
29
- return result;
30
- }