openwriter 0.37.1 → 0.38.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-Dz0iuWDM.css → index-2miZWC8D.css} +1 -1
- package/dist/client/assets/index-C_Zb7mUx.js +215 -0
- package/dist/client/index.html +2 -2
- package/dist/server/content-type-meta.js +1 -0
- package/dist/server/helpers.js +2 -2
- package/dist/server/index.js +3 -0
- package/dist/server/logger.js +2 -2
- package/dist/server/manuscript/assemble.js +128 -0
- package/dist/server/manuscript/index.js +35 -0
- package/dist/server/manuscript/load.js +65 -0
- package/dist/server/manuscript/parse.js +59 -0
- package/dist/server/manuscript/render/css.js +41 -0
- package/dist/server/manuscript/render/docx.js +10 -0
- package/dist/server/manuscript/render/epub.js +156 -0
- package/dist/server/manuscript/render/html.js +91 -0
- package/dist/server/manuscript/render/md.js +41 -0
- package/dist/server/manuscript/resolve.js +42 -0
- package/dist/server/manuscript-routes.js +78 -0
- package/dist/server/mcp.js +78 -4
- package/dist/server/state.js +1 -1
- package/package.json +2 -1
- package/dist/client/assets/index-BBEdpqBq.js +0 -215
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared markdown-it instance for manuscript rendering — matches the
|
|
3
|
+
* configuration in export-routes.ts (the single-doc export path) so manuscript
|
|
4
|
+
* output is consistent with OpenWriter's existing exports: same flavor, same
|
|
5
|
+
* footnote plugin. `xhtml` mode self-closes void elements for EPUB's XHTML.
|
|
6
|
+
*
|
|
7
|
+
* adr: adr/manuscript-engine.md
|
|
8
|
+
*/
|
|
9
|
+
import MarkdownIt from 'markdown-it';
|
|
10
|
+
import markdownItIns from 'markdown-it-ins';
|
|
11
|
+
import markdownItMark from 'markdown-it-mark';
|
|
12
|
+
import markdownItSub from 'markdown-it-sub';
|
|
13
|
+
import markdownItSup from 'markdown-it-sup';
|
|
14
|
+
import markdownItFootnote from 'markdown-it-footnote';
|
|
15
|
+
export function createMd(xhtml = false) {
|
|
16
|
+
const md = new MarkdownIt({ linkify: false, html: true, xhtmlOut: xhtml });
|
|
17
|
+
md.enable('strikethrough');
|
|
18
|
+
md.use(markdownItIns);
|
|
19
|
+
md.use(markdownItMark);
|
|
20
|
+
md.use(markdownItSub);
|
|
21
|
+
md.use(markdownItSup);
|
|
22
|
+
md.use(markdownItFootnote);
|
|
23
|
+
// Chapter anchors: every top-level heading (h1 = a chapter; beats are demoted
|
|
24
|
+
// to h2+) gets a stable sequential id `ch-N`. The assembler's {{toc}} links to
|
|
25
|
+
// these (#ch-N) and the preview's chapter tick-rail also keys off them, so the
|
|
26
|
+
// contents page and the side navigator point at the same targets.
|
|
27
|
+
md.core.ruler.push('chapter_ids', (state) => {
|
|
28
|
+
let n = 0;
|
|
29
|
+
for (const tok of state.tokens) {
|
|
30
|
+
if (tok.type === 'heading_open' && tok.tag === 'h1') {
|
|
31
|
+
n += 1;
|
|
32
|
+
tok.attrSet('id', `ch-${n}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return md;
|
|
37
|
+
}
|
|
38
|
+
/** Render a markdown string to an HTML fragment. */
|
|
39
|
+
export function renderBodyHtml(markdown, xhtml = false) {
|
|
40
|
+
return createMd(xhtml).render(markdown);
|
|
41
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manuscript doc resolution — disk I/O.
|
|
3
|
+
*
|
|
4
|
+
* Maps each manifest doc-pointer (docId) to its current on-disk body. Resolution
|
|
5
|
+
* is by stable docId (rename-safe): filenameByDocId → readFrontmatter().content.
|
|
6
|
+
* The body read from disk is the canonical, ACCEPTED text — pending agent
|
|
7
|
+
* suggestions live in the `_pending/<docId>.json` sidecar, never in the .md body
|
|
8
|
+
* — so a compiled manuscript never ships an un-accepted edit. A docId resolves
|
|
9
|
+
* regardless of which workspace/container the doc lives in (location-independent),
|
|
10
|
+
* which is why a transition doc written "in some random folder" links fine.
|
|
11
|
+
*
|
|
12
|
+
* adr: adr/manuscript-engine.md
|
|
13
|
+
*/
|
|
14
|
+
import { filenameByDocId } from '../documents.js';
|
|
15
|
+
import { readFrontmatter } from '../backlinks.js';
|
|
16
|
+
export function resolveManifestDocs(manifest) {
|
|
17
|
+
const bodyMap = new Map();
|
|
18
|
+
const warnings = [];
|
|
19
|
+
for (const section of manifest.sections) {
|
|
20
|
+
for (const item of section.items) {
|
|
21
|
+
if (item.kind !== 'doc' || !item.docId)
|
|
22
|
+
continue;
|
|
23
|
+
if (bodyMap.has(item.docId))
|
|
24
|
+
continue;
|
|
25
|
+
const filename = filenameByDocId(item.docId);
|
|
26
|
+
if (!filename) {
|
|
27
|
+
warnings.push(`docId ${item.docId} not found (${item.text ?? ''})`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const fm = readFrontmatter(filename);
|
|
31
|
+
if (!fm) {
|
|
32
|
+
warnings.push(`Could not read ${filename} for docId ${item.docId}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
bodyMap.set(item.docId, {
|
|
36
|
+
title: fm.data.title || item.text || 'Untitled',
|
|
37
|
+
body: fm.content,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { bodyMap, warnings };
|
|
42
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manuscript routes: compile + render a manuscript doc to a file or preview.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/manuscript/preview?docId=<id> -> book HTML (iframe source)
|
|
5
|
+
* GET /api/manuscript/export?docId=<id>&format=epub -> epub | docx | html | md
|
|
6
|
+
*
|
|
7
|
+
* Both resolve the manifest doc by stable docId, compile it (resolve pointers +
|
|
8
|
+
* assemble), then render. Preview and export share the same compile() path, so
|
|
9
|
+
* the preview can never disagree with the exported book. adr: adr/manuscript-engine.md
|
|
10
|
+
*/
|
|
11
|
+
import { Router } from 'express';
|
|
12
|
+
import { compileManuscript, renderBookHtml, renderEpub, renderDocx, } from './manuscript/index.js';
|
|
13
|
+
import { listManuscripts, loadManifest, safeName } from './manuscript/load.js';
|
|
14
|
+
export function createManuscriptRouter() {
|
|
15
|
+
const router = Router();
|
|
16
|
+
// Always-on launcher list for the right rail — every manuscript in the profile.
|
|
17
|
+
router.get('/api/manuscripts', (_req, res) => {
|
|
18
|
+
res.json({ manuscripts: listManuscripts() });
|
|
19
|
+
});
|
|
20
|
+
router.get('/api/manuscript/preview', (req, res) => {
|
|
21
|
+
const ms = loadManifest(String(req.query.docId || ''));
|
|
22
|
+
if (!ms)
|
|
23
|
+
return res.status(404).json({ error: 'manuscript doc not found' });
|
|
24
|
+
const { markdown, meta } = compileManuscript(ms.body, ms.meta);
|
|
25
|
+
// The manuscript compose view frames this preview SAME-ORIGIN. The global
|
|
26
|
+
// security gate sends X-Frame-Options: DENY + frame-ancestors 'none', which
|
|
27
|
+
// blocks the iframe entirely. Relax BOTH to same-origin for this route only —
|
|
28
|
+
// a self-contained book page, framed by the app itself, nothing external.
|
|
29
|
+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
30
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; frame-ancestors 'self'");
|
|
31
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
32
|
+
// Screen light/dark follows the app's Appearance setting, passed by the
|
|
33
|
+
// compose view. Export (below) never does — the book ships print-light.
|
|
34
|
+
const mode = req.query.mode === 'dark' ? 'dark' : 'light';
|
|
35
|
+
res.send(renderBookHtml(markdown, meta, mode));
|
|
36
|
+
});
|
|
37
|
+
router.get('/api/manuscript/export', async (req, res) => {
|
|
38
|
+
const ms = loadManifest(String(req.query.docId || ''));
|
|
39
|
+
if (!ms)
|
|
40
|
+
return res.status(404).json({ error: 'manuscript doc not found' });
|
|
41
|
+
const format = String(req.query.format || 'epub').toLowerCase();
|
|
42
|
+
const result = compileManuscript(ms.body, ms.meta);
|
|
43
|
+
const name = safeName(result.meta.title || '');
|
|
44
|
+
try {
|
|
45
|
+
switch (format) {
|
|
46
|
+
case 'epub': {
|
|
47
|
+
const buf = await renderEpub(result.markdown, result.meta);
|
|
48
|
+
res.setHeader('Content-Type', 'application/epub+zip');
|
|
49
|
+
res.setHeader('Content-Disposition', `attachment; filename="${name}.epub"`);
|
|
50
|
+
return res.send(buf);
|
|
51
|
+
}
|
|
52
|
+
case 'docx': {
|
|
53
|
+
const buf = await renderDocx(result.markdown, result.meta);
|
|
54
|
+
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
|
55
|
+
res.setHeader('Content-Disposition', `attachment; filename="${name}.docx"`);
|
|
56
|
+
return res.send(buf);
|
|
57
|
+
}
|
|
58
|
+
case 'html': {
|
|
59
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
60
|
+
res.setHeader('Content-Disposition', `attachment; filename="${name}.html"`);
|
|
61
|
+
return res.send(renderBookHtml(result.markdown, result.meta));
|
|
62
|
+
}
|
|
63
|
+
case 'md': {
|
|
64
|
+
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
|
|
65
|
+
res.setHeader('Content-Disposition', `attachment; filename="${name}.md"`);
|
|
66
|
+
return res.send(result.markdown);
|
|
67
|
+
}
|
|
68
|
+
default:
|
|
69
|
+
return res.status(400).json({ error: `Unknown format: ${format}. Use epub, docx, html, or md.` });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error('[Manuscript] export error:', err?.message);
|
|
74
|
+
return res.status(500).json({ error: 'Manuscript export failed' });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return router;
|
|
78
|
+
}
|
package/dist/server/mcp.js
CHANGED
|
@@ -9,7 +9,9 @@ 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, readConfig } from './helpers.js';
|
|
12
|
+
import { getDataDir, ensureDataDir, resolveDocPath, generateNodeId, atomicWriteFileSync, readConfig, ROOT_DIR } from './helpers.js';
|
|
13
|
+
import { compileManuscript, renderBookHtml, renderEpub, renderDocx } from './manuscript/index.js';
|
|
14
|
+
import { loadManifest, safeName } from './manuscript/load.js';
|
|
13
15
|
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
16
|
import { tiptapToBlocks } from './node-blocks.js';
|
|
15
17
|
import { readBlame, summarizeBlame } from './attribution.js';
|
|
@@ -489,7 +491,7 @@ export const TOOL_REGISTRY = [
|
|
|
489
491
|
workspace: z.string().optional().describe('Workspace title to add this doc to. Creates the workspace if it doesn\'t exist.'),
|
|
490
492
|
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.'),
|
|
491
493
|
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.'),
|
|
492
|
-
content_type: z.enum(['document', 'tweet', 'reply', 'quote', 'article', 'linkedin', 'newsletter', 'blog']).describe('Required. Use "document" for plain documents. Tweet/reply/quote/article/linkedin/newsletter/blog set type-specific metadata automatically.'),
|
|
494
|
+
content_type: z.enum(['document', 'tweet', 'reply', 'quote', 'article', 'linkedin', 'newsletter', 'blog', 'manuscript']).describe('Required. Use "document" for plain documents. Tweet/reply/quote/article/linkedin/newsletter/blog set type-specific metadata automatically. "manuscript" = a binding doc whose body is an ordered list of [text](doc:ID) pointers under ## chapter headings; populate it with the manifest, then it compiles to EPUB/DOCX via the manuscript routes.'),
|
|
493
495
|
url: z.string().optional().describe('Tweet URL — REQUIRED for content_type "reply" or "quote" (e.g. "https://x.com/user/status/123"). Sets tweetContext.url automatically. Ignored for other content types.'),
|
|
494
496
|
afterId: z.string().optional().describe('Place the new doc immediately after this docId (8-char hex) or containerId inside its parent. Omit to append to the bottom of the parent (the default — matches ascending-order convention: newest at bottom). Requires workspace.'),
|
|
495
497
|
status: z.enum(['canonical', 'draft']).optional().describe('Agent-owned lifecycle. "canonical" = committed to spine / load-bearing for the workspace (use for Beats docs that have locked, Research Notes, Master References). "draft" = working / not load-bearing yet / scratch (DUMP docs, first-pass beats). Defaults to "draft" when omitted. Change later via set_metadata({ status: ... }) on lifecycle transitions. v0.19.0.'),
|
|
@@ -728,7 +730,7 @@ export const TOOL_REGISTRY = [
|
|
|
728
730
|
schema: {
|
|
729
731
|
writes: z.array(z.object({
|
|
730
732
|
title: z.string().describe('Title for the document.'),
|
|
731
|
-
content_type: z.enum(['document', 'tweet', 'reply', 'quote', 'article', 'linkedin', 'newsletter', 'blog']).describe('Content type. Use "document" for plain docs.'),
|
|
733
|
+
content_type: z.enum(['document', 'tweet', 'reply', 'quote', 'article', 'linkedin', 'newsletter', 'blog', 'manuscript']).describe('Content type. Use "document" for plain docs. "manuscript" = a binding doc of ordered doc: pointers.'),
|
|
732
734
|
workspace: z.string().optional().describe('Workspace title to add this doc to. Creates the workspace if it does not exist.'),
|
|
733
735
|
container: z.string().optional().describe('Container name within the workspace (e.g. "Chapters"). Requires workspace.'),
|
|
734
736
|
url: z.string().optional().describe('Tweet URL — REQUIRED for content_type "reply" or "quote".'),
|
|
@@ -877,6 +879,78 @@ export const TOOL_REGISTRY = [
|
|
|
877
879
|
return { content: [{ type: 'text', text: `Restored "${result.title}" [${docId}] from archive` }] };
|
|
878
880
|
},
|
|
879
881
|
},
|
|
882
|
+
{
|
|
883
|
+
name: 'compile_manuscript',
|
|
884
|
+
description: 'Compile a manuscript doc (content_type "manuscript") into the assembled master book and report its structure + any problems — WITHOUT writing a file. Resolves every `doc:` pointer in the manifest, concatenates the canonical (accepted) bodies in manifest order under their chapter headings, namespaces footnotes, then returns: title, per-chapter word counts, chapter count, total word count, and warnings. Warnings flag unresolved pointers — a beat that points at a missing/renamed/archived doc — so this is the build-time feedback loop: run it to confirm the binding resolves and see how each chapter is sizing up. Pass includeMarkdown:true to also return the full assembled markdown (large for a real book). Target the manifest by docId (8-char hex).',
|
|
885
|
+
schema: {
|
|
886
|
+
docId: z.string().describe('The manuscript doc (the manifest) by docId (8-char hex from list_documents).'),
|
|
887
|
+
includeMarkdown: z.boolean().optional().describe('Also return the full assembled master markdown. Off by default — for a long book this is very large.'),
|
|
888
|
+
},
|
|
889
|
+
handler: async ({ docId, includeMarkdown }) => {
|
|
890
|
+
const ms = loadManifest(docId);
|
|
891
|
+
if (!ms)
|
|
892
|
+
return { content: [{ type: 'text', text: `No manuscript doc found for docId ${docId}. Is it content_type "manuscript"?` }] };
|
|
893
|
+
const { markdown, meta, warnings } = compileManuscript(ms.body, ms.meta);
|
|
894
|
+
const wc = (s) => { const t = s.trim(); return t ? t.split(/\s+/).length : 0; };
|
|
895
|
+
const chapters = [];
|
|
896
|
+
let cur = null;
|
|
897
|
+
for (const line of markdown.split('\n')) {
|
|
898
|
+
const h = line.match(/^# (.+)/);
|
|
899
|
+
if (h) {
|
|
900
|
+
cur = { title: h[1].trim(), words: 0 };
|
|
901
|
+
chapters.push(cur);
|
|
902
|
+
}
|
|
903
|
+
else if (cur)
|
|
904
|
+
cur.words += wc(line);
|
|
905
|
+
}
|
|
906
|
+
const summary = {
|
|
907
|
+
title: meta.title,
|
|
908
|
+
chapterCount: chapters.length,
|
|
909
|
+
totalWords: wc(markdown),
|
|
910
|
+
chapters,
|
|
911
|
+
warnings,
|
|
912
|
+
};
|
|
913
|
+
if (includeMarkdown)
|
|
914
|
+
summary.markdown = markdown;
|
|
915
|
+
return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] };
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
name: 'export_manuscript',
|
|
920
|
+
description: 'Compile a manuscript doc and WRITE the rendered book to a file: epub (KDP-ready ebook), docx (Word), html (single styled file), or md (the raw assembled master markdown). Same compile + render path as the in-app preview, so the file matches what you see there. Writes to ~/.openwriter/exports/<title>.<ext> by default (or an explicit outputPath) and returns the absolute path plus any compile warnings. EPUB/HTML ship print-light (e-readers handle their own dark mode). Target the manifest by docId (8-char hex).',
|
|
921
|
+
schema: {
|
|
922
|
+
docId: z.string().describe('The manuscript doc (the manifest) by docId (8-char hex from list_documents).'),
|
|
923
|
+
format: z.enum(['epub', 'docx', 'html', 'md']).describe('Output format. epub = KDP-ready ebook; docx = Word; html = single styled file; md = raw assembled master markdown.'),
|
|
924
|
+
outputPath: z.string().optional().describe('Absolute file path to write. Defaults to ~/.openwriter/exports/<title>.<ext>.'),
|
|
925
|
+
},
|
|
926
|
+
handler: async ({ docId, format, outputPath }) => {
|
|
927
|
+
const ms = loadManifest(docId);
|
|
928
|
+
if (!ms)
|
|
929
|
+
return { content: [{ type: 'text', text: `No manuscript doc found for docId ${docId}. Is it content_type "manuscript"?` }] };
|
|
930
|
+
const result = compileManuscript(ms.body, ms.meta);
|
|
931
|
+
const dir = join(ROOT_DIR, 'exports');
|
|
932
|
+
mkdirSync(dir, { recursive: true });
|
|
933
|
+
const outPath = outputPath || join(dir, `${safeName(result.meta.title || '')}.${format}`);
|
|
934
|
+
switch (format) {
|
|
935
|
+
case 'epub':
|
|
936
|
+
writeFileSync(outPath, await renderEpub(result.markdown, result.meta));
|
|
937
|
+
break;
|
|
938
|
+
case 'docx':
|
|
939
|
+
writeFileSync(outPath, await renderDocx(result.markdown, result.meta));
|
|
940
|
+
break;
|
|
941
|
+
case 'html':
|
|
942
|
+
writeFileSync(outPath, renderBookHtml(result.markdown, result.meta), 'utf-8');
|
|
943
|
+
break;
|
|
944
|
+
case 'md':
|
|
945
|
+
writeFileSync(outPath, result.markdown, 'utf-8');
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
const warn = result.warnings.length
|
|
949
|
+
? ` — ${result.warnings.length} warning(s): ${result.warnings.slice(0, 3).join('; ')}${result.warnings.length > 3 ? ' …' : ''}`
|
|
950
|
+
: '';
|
|
951
|
+
return { content: [{ type: 'text', text: `Exported "${result.meta.title}" (${format}) → ${outPath}${warn}` }] };
|
|
952
|
+
},
|
|
953
|
+
},
|
|
880
954
|
{
|
|
881
955
|
name: 'get_metadata',
|
|
882
956
|
description: 'Get the JSON frontmatter metadata for a document. Returns all key-value pairs stored in frontmatter (title, summary, characters, tags, etc.). Useful for understanding document context without reading full content.',
|
|
@@ -1358,7 +1432,7 @@ export const TOOL_REGISTRY = [
|
|
|
1358
1432
|
settings: z.record(z.string()).optional().describe('Setting name → description (merged)'),
|
|
1359
1433
|
rules: z.array(z.string()).optional().describe('Writing rules for this workspace (replaces)'),
|
|
1360
1434
|
logline: z.string().nullable().optional().describe('One-sentence "what this workspace is for". Set null to clear.'),
|
|
1361
|
-
domain: z.string().nullable().optional().describe('Subject area (e.g. "
|
|
1435
|
+
domain: z.string().nullable().optional().describe('Subject area (e.g. "Marine biology"). Set null to clear.'),
|
|
1362
1436
|
schema: z.string().nullable().optional().describe('Workspace kind: book / concept-library / inbox / social / reference. Set null to clear.'),
|
|
1363
1437
|
vocab: z.array(z.string()).nullable().optional().describe('Closed list of valid domain names — Haiku classifies docs INTO these. Set null to clear (opens vocab to free-form).'),
|
|
1364
1438
|
relatedWorkspaces: z.array(z.string()).nullable().optional().describe('Sibling workspace filenames. Set null to clear.'),
|
package/dist/server/state.js
CHANGED
|
@@ -831,7 +831,7 @@ export function mergeMetadataUpdates(existing, updates) {
|
|
|
831
831
|
return null;
|
|
832
832
|
}
|
|
833
833
|
// Deep-merge known context objects
|
|
834
|
-
const CONTEXT_KEYS = ['blogContext', 'newsletterContext', 'articleContext', 'tweetContext', 'linkedinContext'];
|
|
834
|
+
const CONTEXT_KEYS = ['blogContext', 'newsletterContext', 'articleContext', 'tweetContext', 'linkedinContext', 'manuscriptContext'];
|
|
835
835
|
for (const key of CONTEXT_KEYS) {
|
|
836
836
|
if (updates[key] && typeof updates[key] === 'object' && existing?.[key] && typeof existing[key] === 'object') {
|
|
837
837
|
updates[key] = { ...existing[key], ...updates[key] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openwriter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"@xdevplatform/xdk": "^0.4.0",
|
|
64
64
|
"express": "^4.21.0",
|
|
65
65
|
"gray-matter": "^4.0.3",
|
|
66
|
+
"jszip": "^3.10.1",
|
|
66
67
|
"lowlight": "^3.3.0",
|
|
67
68
|
"markdown-it": "^14.1.1",
|
|
68
69
|
"markdown-it-footnote": "^4.0.0",
|