openwriter 0.31.0 → 0.33.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.
@@ -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-CaFrYDJP.js"></script>
14
- <link rel="stylesheet" crossorigin href="/assets/index-C-eMDCqj.css">
13
+ <script type="module" crossorigin src="/assets/index-yRAovDFi.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/assets/index-BFw23tzV.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -53,10 +53,10 @@ const plugin = {
53
53
  description: 'Writing model',
54
54
  options: [
55
55
  { value: '', label: 'Default (Strongest)' },
56
- { value: 'strongest', label: 'Strongest — Claude Opus (best quality)' },
57
- { value: 'balanced', label: 'Balanced — Claude Sonnet' },
58
- { value: 'fast-plus', label: 'Fast+ — Gemini 3.5 Flash (newest, great)' },
59
- { value: 'fast', label: 'Fast — Gemini 2.5 Flash (cheapest)' },
56
+ { value: 'strongest', label: 'Strongest — Claude Opus (best quality, ~20¢/edit)' },
57
+ { value: 'balanced', label: 'Balanced — Claude Sonnet (~3¢/edit)' },
58
+ { value: 'fast-plus', label: 'Fast+ — Gemini 3.5 Flash (newest, great, ~1¢/edit)' },
59
+ { value: 'fast', label: 'Fast — Gemini 2.5 Flash (free)' },
60
60
  ],
61
61
  },
62
62
  },
@@ -121,6 +121,35 @@ const plugin = {
121
121
  res.status(502).json({ error: 'AV backend unreachable' });
122
122
  }
123
123
  });
124
+ // Wildcard GET proxy for /api/voice/* — same key-injection + base-URL pattern as the POST
125
+ // proxy above, no body. Covers the wallet/top-up reads (GET /api/voice/billing and
126
+ // /api/voice/billing/topup-options); the matching POST /api/voice/billing/topup rides the
127
+ // POST wildcard. Query string is forwarded so any future paginated read just works.
128
+ ctx.app.get('/api/voice/*', async (req, res) => {
129
+ try {
130
+ const subPath = req.params[0] || '';
131
+ const qs = req.originalUrl.includes('?') ? req.originalUrl.slice(req.originalUrl.indexOf('?')) : '';
132
+ const targetUrl = `${backendUrl}/api/voice/${subPath}${qs}`;
133
+ console.log(`[AV Plugin] ${req.method} ${req.path} → ${targetUrl}`);
134
+ const upstream = await fetch(targetUrl, {
135
+ method: 'GET',
136
+ headers: authHeaders(),
137
+ });
138
+ res.status(upstream.status);
139
+ const responseText = await upstream.text();
140
+ try {
141
+ res.json(JSON.parse(responseText));
142
+ }
143
+ catch {
144
+ console.error('[AV Plugin] Non-JSON response:', responseText.substring(0, 500));
145
+ res.status(502).json({ error: 'AV backend returned non-JSON response' });
146
+ }
147
+ }
148
+ catch (err) {
149
+ console.error('[AV Plugin] Backend error:', err?.message || err);
150
+ res.status(502).json({ error: 'AV backend unreachable' });
151
+ }
152
+ });
124
153
  },
125
154
  contextMenuItems() {
126
155
  return [
@@ -8,7 +8,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
8
8
  import { randomUUID } from 'crypto';
9
9
  import { homedir } from 'os';
10
10
  /** Fallback: generate image via the publish platform API, download and save locally */
11
- async function generateViaPlatform(prompt, dataDir, aspectRatio = '16:9') {
11
+ async function generateViaPlatform(prompt, dataDir, imageApiKey, aspectRatio = '16:9') {
12
12
  const configPath = join(homedir(), '.openwriter', 'config.json');
13
13
  if (!existsSync(configPath))
14
14
  return null;
@@ -26,6 +26,8 @@ async function generateViaPlatform(prompt, dataDir, aspectRatio = '16:9') {
26
26
  'Content-Type': 'application/json',
27
27
  Authorization: `Bearer ${platformKey}`,
28
28
  'X-Profile': profile,
29
+ // BYO image key → worker skips the shared-key allotment and bills the user's own key (uncapped).
30
+ ...(imageApiKey ? { 'X-Image-Key': imageApiKey } : {}),
29
31
  },
30
32
  body: JSON.stringify({ prompt, aspect_ratio: aspectRatio }),
31
33
  });
@@ -57,6 +59,11 @@ const plugin = {
57
59
  required: false,
58
60
  description: 'Google Gemini API key for image generation (optional — falls back to publish platform)',
59
61
  },
62
+ imageApiKey: {
63
+ type: 'string',
64
+ required: false,
65
+ description: "Your own image API key (Gemini) — unlimited generations at your cost. Leave blank to use OpenWriter's included allotment.",
66
+ },
60
67
  },
61
68
  registerRoutes(ctx) {
62
69
  ctx.app.post('/api/image-gen/generate', async (req, res) => {
@@ -97,8 +104,9 @@ const plugin = {
97
104
  }
98
105
  }
99
106
  else {
100
- // Fallback: generate via publish platform API
101
- const platformResult = await generateViaPlatform(prompt, ctx.dataDir);
107
+ // Fallback: generate via publish platform API.
108
+ // Pass the user's own image key (if set) so the worker bills it instead of the shared allotment.
109
+ const platformResult = await generateViaPlatform(prompt, ctx.dataDir, ctx.config['imageApiKey']);
102
110
  if (!platformResult) {
103
111
  res.status(400).json({ success: false, error: 'No GEMINI_API_KEY and publish platform not configured. Set GEMINI_API_KEY or log in to the publish plugin.' });
104
112
  return;
@@ -976,7 +976,7 @@ const plugin = {
976
976
  // --- Billing tools ---
977
977
  {
978
978
  name: 'get_billing',
979
- description: 'Get current subscription plan, feature limits, and billing status. Shows plan tier, post/subscriber limits, and whether payment is active.',
979
+ description: 'Get current subscription plan, capabilities, and billing status. Shows the plan tier (Publish or Publish+Email), which publishing channels are enabled, and whether payment is active.',
980
980
  inputSchema: { type: 'object', properties: {} },
981
981
  handler: async () => {
982
982
  const res = await publishFetch(config, '/billing');
@@ -989,11 +989,11 @@ const plugin = {
989
989
  },
990
990
  {
991
991
  name: 'upgrade_plan',
992
- description: 'Get a Stripe Checkout URL to subscribe or upgrade. Opens in browser for payment. Plans: creator ($19/mo), growth ($49/mo), publisher ($79/mo). Period: monthly or annual (2 months free).',
992
+ description: 'Get a Stripe Checkout URL to subscribe or upgrade. Opens in browser for payment. Plans (wire value = label): "creator" = Publish ($19/mo, $190/yr) — the scheduler + publishing to every channel except email (X, LinkedIn, WordPress, Ghost, Beehiiv, blog); "growth" = Publish+Email ($49/mo, $490/yr) — everything in Publish plus newsletter/email. Period: monthly or annual (2 months free).',
993
993
  inputSchema: {
994
994
  type: 'object',
995
995
  properties: {
996
- plan: { type: 'string', enum: ['creator', 'growth', 'publisher'], description: 'Plan to subscribe to' },
996
+ plan: { type: 'string', enum: ['creator', 'growth'], description: 'Plan to subscribe to: "creator" = Publish ($19/mo), "growth" = Publish+Email ($49/mo)' },
997
997
  period: { type: 'string', enum: ['monthly', 'annual'], description: 'Billing period (default: monthly). Annual saves 2 months.' },
998
998
  },
999
999
  required: ['plan'],
@@ -1011,9 +1011,10 @@ const plugin = {
1011
1011
  return { error: `Checkout failed: ${err.error || res.statusText}` };
1012
1012
  }
1013
1013
  const data = await res.json();
1014
+ const planLabel = params.plan === 'growth' ? 'Publish+Email' : 'Publish';
1014
1015
  return {
1015
1016
  url: data.url,
1016
- message: `Open this URL to complete your ${params.plan} subscription: ${data.url}`,
1017
+ message: `Open this URL to complete your ${planLabel} subscription: ${data.url}`,
1017
1018
  };
1018
1019
  },
1019
1020
  },
@@ -33,7 +33,7 @@ import { createBillingRouter } from './billing-routes.js';
33
33
  import { createTaskRouter } from './task-routes.js';
34
34
  import { platformFetch, isAuthenticated } from './connections.js';
35
35
  import { PluginManager } from './plugin-manager.js';
36
- import { checkForUpdate, getUpdateInfo, getCurrentVersion } from './update-check.js';
36
+ import { checkForUpdate, getUpdateInfo, getCurrentVersion, getInstallType, getUpdateCommand } from './update-check.js';
37
37
  import { addComment, getComments, resolveComments, unresolveComments, deleteComments, editComment } from './comments.js';
38
38
  import { initLogger, logger, generateRequestId, withRequestId } from './logger.js';
39
39
  const __filename = fileURLToPath(import.meta.url);
@@ -101,7 +101,12 @@ export async function startHttpServer(options = {}) {
101
101
  });
102
102
  app.get('/api/update-info', (_req, res) => {
103
103
  const latestVersion = getUpdateInfo();
104
- res.json({ updateAvailable: latestVersion, currentVersion: getCurrentVersion() });
104
+ res.json({
105
+ updateAvailable: latestVersion,
106
+ currentVersion: getCurrentVersion(),
107
+ installType: getInstallType(),
108
+ updateCommand: getUpdateCommand(),
109
+ });
105
110
  });
106
111
  app.get('/api/document', (_req, res) => {
107
112
  res.json({ document: getDocument(), title: getTitle(), metadata: getMetadata() });
@@ -9,7 +9,7 @@ import { randomUUID } from 'crypto';
9
9
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
10
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
11
  import { z } from 'zod';
12
- import { getDataDir, ensureDataDir, resolveDocPath, generateNodeId, atomicWriteFileSync } from './helpers.js';
12
+ import { getDataDir, ensureDataDir, resolveDocPath, generateNodeId, atomicWriteFileSync, readConfig } from './helpers.js';
13
13
  import { getDocument, getWordCount, getPendingChangeCount, getTitle, getStatus, getNodesByIds, findNodesByIds, getMetadata, setMetadata, mergeMetadataUpdates, applyChanges, applyTextEdits, updateDocument, save, markAllNodesAsPending, setAgentLock, setAgentLockActive, updatePendingCacheForActiveDoc, populateDocumentFile, applyChangesToFile, applyTextEditsToFile, getDocId, getFilePath, getIsTemp, extractText, countPending, addDocTag, removeDocTag, getCachedDocument, invalidateDocCache, isAutoAcceptActive, removePendingCacheEntry, getExternalMtimeDrift, reloadActiveDocFromDisk, getCanonical, cloneWithPendingReverted, bumpDocVersion, setSortProposalOnFile, clearSortRequestOnFile, } from './state.js';
14
14
  import { tiptapToBlocks } from './node-blocks.js';
15
15
  import { outline, peek, searchInDoc, truncateRead } from './peek-outline.js';
@@ -1594,9 +1594,13 @@ export const TOOL_REGISTRY = [
1594
1594
  }
1595
1595
  return { content: [{ type: 'text', text: 'Error: No GEMINI_API_KEY and publish platform not configured. Set GEMINI_API_KEY or log in to the publish plugin.' }] };
1596
1596
  }
1597
+ // BYO image key (image-gen plugin config) → worker uses the user's own key, uncapped.
1598
+ // Blank → shared-key allotment applies. Key is never logged or echoed.
1599
+ const userImageKey = readConfig().plugins?.['@openwriter/plugin-image-gen']?.config?.['imageApiKey'] || '';
1597
1600
  const res = await platformFetch('/images/generate', {
1598
1601
  method: 'POST',
1599
1602
  body: JSON.stringify({ prompt, aspect_ratio: aspect_ratio || '16:9' }),
1603
+ ...(userImageKey ? { headers: { 'X-Image-Key': userImageKey } } : {}),
1600
1604
  });
1601
1605
  if (!res.ok) {
1602
1606
  const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
@@ -3,7 +3,7 @@
3
3
  * Uses Node's built-in fetch + existing config system.
4
4
  * Fire-and-forget: never blocks startup, never throws to caller.
5
5
  */
6
- import { readFileSync } from 'fs';
6
+ import { readFileSync, existsSync } from 'fs';
7
7
  import { join, dirname } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { readConfig, saveConfig } from './helpers.js';
@@ -38,6 +38,33 @@ export function getCurrentVersion() {
38
38
  return '0.0.0';
39
39
  }
40
40
  }
41
+ /**
42
+ * Detect how this instance was installed.
43
+ * - 'git' — running from a git checkout (dev / dogfood): a `.git` dir exists
44
+ * above the package. Update path is `git pull && npm run build`.
45
+ * - 'npm' — packaged global install (npm tarball extract, no `.git`).
46
+ * Update path is `npm update -g openwriter`.
47
+ * Walks up from the package dir; npm tarballs never ship `.git`, so its
48
+ * absence is a reliable signal.
49
+ */
50
+ export function getInstallType() {
51
+ let dir = __dirname;
52
+ for (let i = 0; i < 8; i++) {
53
+ if (existsSync(join(dir, '.git')))
54
+ return 'git';
55
+ const parent = dirname(dir);
56
+ if (parent === dir)
57
+ break; // hit filesystem root
58
+ dir = parent;
59
+ }
60
+ return 'npm';
61
+ }
62
+ /** The shell command this user should run to update, given their install type. */
63
+ export function getUpdateCommand() {
64
+ return getInstallType() === 'git'
65
+ ? 'git pull && npm run build'
66
+ : 'npm update -g openwriter';
67
+ }
41
68
  /**
42
69
  * Check npm registry for a newer version. Fire-and-forget.
43
70
  * - Respects NO_UPDATE_NOTIFIER env var
@@ -56,7 +83,7 @@ export async function checkForUpdate() {
56
83
  if (now - lastCheck < CHECK_INTERVAL_MS) {
57
84
  if (compareVersions(currentVersion, config.latestVersion) < 0) {
58
85
  cachedLatestVersion = config.latestVersion;
59
- console.error(`[OpenWriter] Update available: ${currentVersion} → ${config.latestVersion} — run: npm update -g openwriter`);
86
+ console.error(`[OpenWriter] Update available: ${currentVersion} → ${config.latestVersion} — run: ${getUpdateCommand()}`);
60
87
  }
61
88
  return;
62
89
  }
@@ -82,7 +109,7 @@ export async function checkForUpdate() {
82
109
  });
83
110
  if (compareVersions(currentVersion, latestVersion) < 0) {
84
111
  cachedLatestVersion = latestVersion;
85
- console.error(`[OpenWriter] Update available: ${currentVersion} → ${latestVersion} — run: npm update -g openwriter`);
112
+ console.error(`[OpenWriter] Update available: ${currentVersion} → ${latestVersion} — run: ${getUpdateCommand()}`);
86
113
  }
87
114
  }
88
115
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openwriter",
3
- "version": "0.31.0",
3
+ "version": "0.33.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",