openwriter 0.8.3 → 0.8.6

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.
@@ -5,18 +5,32 @@ import markdownItSub from 'markdown-it-sub';
5
5
  import markdownItSup from 'markdown-it-sup';
6
6
  import { readFileSync, existsSync } from 'fs';
7
7
  import { join, extname } from 'path';
8
- // Lazy-load server modules at runtime (same process, resolved from monorepo root)
9
- const baseDir = new URL('../../../packages/openwriter/dist/server/', import.meta.url).href;
8
+ // Lazy-load server modules at runtime
9
+ // npm package: dist/plugins/publish/dist/helpers.js → ../../../server/
10
+ // Monorepo: plugins/publish/dist/helpers.js → ../../../packages/openwriter/dist/server/
11
+ const npmBase = new URL('../../../server/', import.meta.url).href;
12
+ const monoBase = new URL('../../../packages/openwriter/dist/server/', import.meta.url).href;
10
13
  let _cached = null;
14
+ async function tryImport(base) {
15
+ const [markdown, state, helpers, connections] = await Promise.all([
16
+ import(base + 'markdown.js'),
17
+ import(base + 'state.js'),
18
+ import(base + 'helpers.js'),
19
+ import(base + 'connections.js'),
20
+ ]);
21
+ return { markdown, state, helpers, connections };
22
+ }
11
23
  export async function getServerModules() {
12
24
  if (_cached)
13
25
  return _cached;
14
- const [markdown, state, helpers, connections] = await Promise.all([
15
- import(baseDir + 'markdown.js'),
16
- import(baseDir + 'state.js'),
17
- import(baseDir + 'helpers.js'),
18
- import(baseDir + 'connections.js'),
19
- ]);
26
+ // Try npm package layout first, fall back to monorepo layout
27
+ let markdown, state, helpers, connections;
28
+ try {
29
+ ({ markdown, state, helpers, connections } = await tryImport(npmBase));
30
+ }
31
+ catch {
32
+ ({ markdown, state, helpers, connections } = await tryImport(monoBase));
33
+ }
20
34
  _cached = {
21
35
  tiptapToMarkdown: markdown.tiptapToMarkdown,
22
36
  getDocument: state.getDocument,
@@ -438,7 +438,7 @@ export function createDocument(title, content, path) {
438
438
  }
439
439
  }
440
440
  else {
441
- newDoc = { type: 'doc', content: [{ type: 'paragraph', content: [] }] };
441
+ newDoc = { type: 'doc', content: [{ type: 'paragraph', attrs: { id: generateNodeId() }, content: [] }] };
442
442
  }
443
443
  const metadata = { title: docTitle, docId: generateNodeId() };
444
444
  setActiveDocument(newDoc, docTitle, filePath, isTemp, undefined, metadata);
@@ -489,7 +489,7 @@ export function createDocumentFile(title, path, extraMeta) {
489
489
  }
490
490
  filename = filePath.split(/[/\\]/).pop();
491
491
  }
492
- const newDoc = { type: 'doc', content: [{ type: 'paragraph', content: [] }] };
492
+ const newDoc = { type: 'doc', content: [{ type: 'paragraph', attrs: { id: generateNodeId() }, content: [] }] };
493
493
  const metadata = { title: docTitle, docId: generateNodeId(), agentCreated: true, ...extraMeta };
494
494
  const markdown = tiptapToMarkdown(newDoc, docTitle, metadata);
495
495
  ensureDataDir();
@@ -8,34 +8,44 @@ const __dirname = path.dirname(__filename);
8
8
  function log(msg) {
9
9
  console.error(msg);
10
10
  }
11
- function isGloballyInstalled() {
11
+ function getGlobalVersion() {
12
12
  try {
13
- const result = execSync('openwriter --version', {
13
+ return execSync('openwriter --version', {
14
14
  stdio: ['pipe', 'pipe', 'pipe'],
15
15
  timeout: 10000,
16
- });
17
- return true;
16
+ }).toString().trim();
18
17
  }
19
18
  catch {
20
- return false;
19
+ return null;
21
20
  }
22
21
  }
23
- function installGlobally() {
24
- log('\n② Installing openwriter globally...');
22
+ function getLatestVersion() {
23
+ try {
24
+ return execSync('npm view openwriter version', {
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ timeout: 15000,
27
+ }).toString().trim();
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ function installOrUpdateGlobally() {
34
+ log('\n② Installing/updating openwriter globally...');
25
35
  // Try without sudo first (works on Windows, nvm, Homebrew, volta, etc.)
26
36
  try {
27
- execSync('npm install -g openwriter', { stdio: 'inherit', timeout: 120000 });
37
+ execSync('npm install -g openwriter@latest', { stdio: 'inherit', timeout: 120000 });
28
38
  log(' ✓ Installed globally');
29
39
  return true;
30
40
  }
31
41
  catch {
32
42
  // Likely permission error on macOS/Linux system Node
33
43
  }
34
- // Try with sudo on non-Windows
35
- if (process.platform !== 'win32') {
44
+ // Try with sudo on non-Windows (only if we have a TTY for password prompt)
45
+ if (process.platform !== 'win32' && process.stdin.isTTY) {
36
46
  log(' Retrying with sudo...');
37
47
  try {
38
- execSync('sudo npm install -g openwriter', { stdio: 'inherit', timeout: 120000 });
48
+ execSync('sudo npm install -g openwriter@latest', { stdio: 'inherit', timeout: 120000 });
39
49
  log(' ✓ Installed globally (sudo)');
40
50
  return true;
41
51
  }
@@ -43,8 +53,7 @@ function installGlobally() {
43
53
  // sudo failed too
44
54
  }
45
55
  }
46
- log(' ✗ Could not install globally. Run manually:');
47
- log(' npm install -g openwriter');
56
+ log(' ✗ Could not install globally (permissions)');
48
57
  return false;
49
58
  }
50
59
  function isMcpConfigured() {
@@ -59,11 +68,16 @@ function isMcpConfigured() {
59
68
  return false;
60
69
  }
61
70
  }
62
- function configureMcp() {
71
+ function configureMcp(useNpx) {
63
72
  log('\n③ Configuring MCP server for Claude Code...');
73
+ const mcpCommand = useNpx ? 'npx' : 'openwriter';
74
+ const mcpArgs = useNpx ? ['-y', 'openwriter', '--no-open'] : ['--no-open'];
64
75
  // Try using claude CLI
76
+ const cliArgs = useNpx
77
+ ? 'claude mcp add -s user openwriter -- npx -y openwriter --no-open'
78
+ : 'claude mcp add -s user openwriter -- openwriter --no-open';
65
79
  try {
66
- execSync('claude mcp add -s user openwriter -- openwriter --no-open', {
80
+ execSync(cliArgs, {
67
81
  stdio: 'inherit',
68
82
  timeout: 15000,
69
83
  });
@@ -86,7 +100,7 @@ function configureMcp() {
86
100
  // Add openwriter as first entry (Claude Code loads sequentially)
87
101
  const existing = config.mcpServers;
88
102
  config.mcpServers = {
89
- openwriter: { command: 'openwriter', args: ['--no-open'] },
103
+ openwriter: { command: mcpCommand, args: mcpArgs },
90
104
  ...existing,
91
105
  };
92
106
  fs.writeFileSync(claudeJson, JSON.stringify(config, null, 2), 'utf-8');
@@ -122,22 +136,29 @@ export function installSkill() {
122
136
  }
123
137
  log(` ✓ Skill docs copied to ${docsTarget}`);
124
138
  }
125
- // Step 2: Global install (skip if already installed)
126
- const alreadyInstalled = isGloballyInstalled();
127
- if (alreadyInstalled) {
128
- log('\n② openwriter already installed globally — skipping');
139
+ // Step 2: Global install or update
140
+ let useNpx = false;
141
+ const currentVersion = getGlobalVersion();
142
+ const latestVersion = getLatestVersion();
143
+ if (currentVersion && latestVersion && currentVersion === latestVersion) {
144
+ log(`\n② openwriter v${currentVersion} is up to date — skipping`);
129
145
  }
130
146
  else {
131
- if (!installGlobally()) {
132
- process.exit(1);
147
+ if (currentVersion && latestVersion) {
148
+ log(`\n② Updating openwriter: v${currentVersion} → v${latestVersion}`);
149
+ }
150
+ if (!installOrUpdateGlobally()) {
151
+ // Global install/update failed (permissions) — fall back to npx
152
+ log(' → Will use npx fallback for MCP server (no global install needed)');
153
+ useNpx = true;
133
154
  }
134
155
  }
135
- // Step 3: Configure MCP server (skip if already configured)
136
- if (isMcpConfigured()) {
156
+ // Step 3: Configure MCP server
157
+ if (isMcpConfigured() && !useNpx) {
137
158
  log('\n③ MCP server already configured — skipping');
138
159
  }
139
160
  else {
140
- configureMcp();
161
+ configureMcp(useNpx);
141
162
  }
142
163
  // Done
143
164
  log('\n✓ OpenWriter is ready!');
@@ -271,13 +271,13 @@ export const TOOL_REGISTRY = [
271
271
  },
272
272
  {
273
273
  name: 'create_document',
274
- description: 'Create a new empty document and switch to it. Always provide a title. Saves the current document first. By default shows a sidebar spinner that persists until populate_document is called set empty=true to skip the spinner and switch immediately (use for template docs like tweets/articles that don\'t need agent content). If workspace is provided, the doc is automatically added to it (workspace is created if it doesn\'t exist). If container is also provided, the doc is placed inside that container (created if it doesn\'t exist). Use content_type to create typed documents (tweet, article, linkedin, etc.) with the correct metadata pre-set.',
274
+ description: 'Create a new document. Always provide a title. By default shows a sidebar spinner call populate_document next to deliver content and clear it. This two-step flow is REQUIRED for all content documents: create_document → populate_document. Do NOT use empty=true to skip this empty=true is ONLY for content_type template docs (tweets, articles) that start blank and get metadata via set_metadata. If workspace is provided, the doc is automatically added to it (workspace is created if it doesn\'t exist). If container is also provided, the doc is placed inside that container (created if it doesn\'t exist).',
275
275
  schema: {
276
276
  title: z.string().optional().describe('Title for the new document. Defaults to "Untitled".'),
277
277
  path: z.string().optional().describe('Absolute file path to create the document at (e.g. "C:/projects/doc.md"). If omitted, creates in ~/.openwriter/.'),
278
278
  workspace: z.string().optional().describe('Workspace title to add this doc to. Creates the workspace if it doesn\'t exist.'),
279
279
  container: z.string().optional().describe('Container name within the workspace (e.g. "Chapters", "Notes", "References"). Creates the container if it doesn\'t exist. Requires workspace.'),
280
- empty: z.boolean().optional().describe('If true, skip the writing spinner and switch to the doc immediately. No need to call populate_document. Use for template docs (tweets, articles) that start empty.'),
280
+ empty: z.boolean().optional().describe('ONLY for content_type template docs (tweets, articles) that start blank. Skips the spinner and switches immediately. Do NOT set this for content documents use the two-step flow (create_document → populate_document) instead.'),
281
281
  content_type: z.string().optional().describe('Content type: tweet, reply, quote, article, linkedin, newsletter, or blog. Sets metadata so the doc is recognized as that type. For reply/quote, use set_metadata after creation to set the target tweet URL.'),
282
282
  },
283
283
  handler: async ({ title, path, workspace, container, empty, content_type }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openwriter",
3
- "version": "0.8.3",
3
+ "version": "0.8.6",
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",