openwriter 0.29.0 → 0.29.1
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/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
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-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-DHaZI7nA.js"></script>
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/index-Gdw1m46J.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Single source of truth for content-type scaffolding metadata. Maps a
|
|
2
|
+
// content_type to the frontmatter it needs: the `content_type` field itself
|
|
3
|
+
// (which owns the editor surface — adr: adr/browser-write-fidelity.md) plus the
|
|
4
|
+
// type's context object (tweetContext / articleContext / blogContext / …).
|
|
5
|
+
//
|
|
6
|
+
// Used by both the MCP create_document handler and the HTTP POST /api/documents
|
|
7
|
+
// endpoint (the "Create variant" path) so a typed empty doc is scaffolded the
|
|
8
|
+
// same way regardless of who creates it. adr: docs/variants.md
|
|
9
|
+
export function resolveTypeMeta(type, url) {
|
|
10
|
+
switch (type) {
|
|
11
|
+
case 'tweet': return { content_type: 'tweet', tweetContext: { mode: 'tweet' } };
|
|
12
|
+
case 'reply': return { content_type: 'reply', tweetContext: { mode: 'reply', ...(url ? { url } : {}) } };
|
|
13
|
+
case 'quote': return { content_type: 'quote', tweetContext: { mode: 'quote', ...(url ? { url } : {}) } };
|
|
14
|
+
case 'article': return { content_type: 'article', articleContext: { active: true } };
|
|
15
|
+
case 'linkedin': return { content_type: 'linkedin', linkedinContext: { active: true } };
|
|
16
|
+
case 'newsletter': return { content_type: 'newsletter', newsletterContext: { active: true } };
|
|
17
|
+
case 'blog': return { content_type: 'blog', blogContext: { active: true } };
|
|
18
|
+
default: return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/server/documents.js
CHANGED
|
@@ -8,6 +8,7 @@ import { join } from 'path';
|
|
|
8
8
|
import matter from 'gray-matter';
|
|
9
9
|
import trash from 'trash';
|
|
10
10
|
import { tiptapToMarkdownChecked, markdownToTiptap } from './markdown.js';
|
|
11
|
+
import { resolveTypeMeta } from './content-type-meta.js';
|
|
11
12
|
import { parseMarkdownContent } from './compact.js';
|
|
12
13
|
import { getDocument, getTitle, getFilePath, getIsTemp, getMetadata, save, cancelDebouncedSave, setActiveDocument, registerExternalDoc, unregisterExternalDoc, getExternalDocs, cacheActiveDocument, getCachedDocument, invalidateDocCache, removePendingCacheEntry, resetDocVersion, markAsAgentStub, unmarkAgentStub, isAgentStub, } from './state.js';
|
|
13
14
|
import { getDataDir, TEMP_PREFIX, ensureDataDir, filePathForTitle, tempFilePath, generateNodeId, resolveDocPath, isExternalDoc, atomicWriteFileSync, canonicalizePath } from './helpers.js';
|
|
@@ -1148,6 +1149,77 @@ export function duplicateDocument(filename, variant) {
|
|
|
1148
1149
|
const newFilename = filePath.split(/[/\\]/).pop();
|
|
1149
1150
|
return { document: getDocument(), title: getTitle(), filename: newFilename };
|
|
1150
1151
|
}
|
|
1152
|
+
// Content types that surface an editable title/headline above the body. For
|
|
1153
|
+
// these, the doc's frontmatter `title` IS content (blog headline, article title,
|
|
1154
|
+
// newsletter subject). For every other type the title is just a sidebar label
|
|
1155
|
+
// and the body carries everything. adr: docs/variants.md
|
|
1156
|
+
const TITLE_BEARING_TYPES = new Set(['blog', 'article', 'newsletter']);
|
|
1157
|
+
/**
|
|
1158
|
+
* Create a variant of `masterFilename` retyped as `variantType`, nested under
|
|
1159
|
+
* the master. Field-projection model (NOT a verbatim clone — that's
|
|
1160
|
+
* duplicateDocument): port the fields the two types share.
|
|
1161
|
+
* - body: always ported.
|
|
1162
|
+
* - downcast (title-bearing master → body-only variant): the master's title is
|
|
1163
|
+
* folded into the body as its first paragraph so the headline isn't lost.
|
|
1164
|
+
* - the variant is scaffolded with the TARGET type's content_type + context;
|
|
1165
|
+
* the source's context objects (blogContext, tweetContext, …) are NOT
|
|
1166
|
+
* inherited — a variant is a new typed doc, not a surface clone.
|
|
1167
|
+
* adr: docs/variants.md
|
|
1168
|
+
*/
|
|
1169
|
+
export function createVariant(masterFilename, opts) {
|
|
1170
|
+
cancelDebouncedSave();
|
|
1171
|
+
save();
|
|
1172
|
+
const sourcePath = resolveDocPath(masterFilename);
|
|
1173
|
+
if (!existsSync(sourcePath))
|
|
1174
|
+
throw new Error(`Document not found: ${masterFilename}`);
|
|
1175
|
+
const raw = readFileSync(sourcePath, 'utf-8');
|
|
1176
|
+
const parsed = markdownToTiptap(raw);
|
|
1177
|
+
const srcType = deriveContentType(parsed.metadata) || 'document';
|
|
1178
|
+
const tgtType = opts.variantType;
|
|
1179
|
+
const srcTitleBearing = TITLE_BEARING_TYPES.has(srcType);
|
|
1180
|
+
const tgtTitleBearing = TITLE_BEARING_TYPES.has(tgtType);
|
|
1181
|
+
// Body projection. Downcast (title-bearing → body-only): prepend the master's
|
|
1182
|
+
// title as the first paragraph so the headline survives in a surface with no
|
|
1183
|
+
// title field ("title becomes first line, body the next paragraph"). Otherwise
|
|
1184
|
+
// the body ports unchanged.
|
|
1185
|
+
let bodyContent = parsed.document.content || [];
|
|
1186
|
+
if (srcTitleBearing && !tgtTitleBearing && parsed.title) {
|
|
1187
|
+
bodyContent = [
|
|
1188
|
+
{ type: 'paragraph', content: [{ type: 'text', text: parsed.title }] },
|
|
1189
|
+
...bodyContent,
|
|
1190
|
+
];
|
|
1191
|
+
}
|
|
1192
|
+
const bodyDoc = { ...parsed.document, content: bodyContent };
|
|
1193
|
+
// Title is always label-suffixed: it doubles as the filename + sidebar name,
|
|
1194
|
+
// so it must stay unique vs the master (a raw duplicate title would collide).
|
|
1195
|
+
// The title CONTENT still rides along for title-bearing targets — they render
|
|
1196
|
+
// it as the headline and the user trims the suffix.
|
|
1197
|
+
const Label = tgtType.charAt(0).toUpperCase() + tgtType.slice(1);
|
|
1198
|
+
let newTitle = `${parsed.title} (${Label})`;
|
|
1199
|
+
let filePath = filePathForTitle(newTitle);
|
|
1200
|
+
if (existsSync(filePath)) {
|
|
1201
|
+
let counter = 2;
|
|
1202
|
+
while (existsSync(filePathForTitle(`${parsed.title} (${Label} ${counter})`)))
|
|
1203
|
+
counter++;
|
|
1204
|
+
newTitle = `${parsed.title} (${Label} ${counter})`;
|
|
1205
|
+
filePath = filePathForTitle(newTitle);
|
|
1206
|
+
}
|
|
1207
|
+
// Fresh metadata: target type scaffold + variant relationship only. Source
|
|
1208
|
+
// context objects are intentionally dropped (see header).
|
|
1209
|
+
const metadata = {
|
|
1210
|
+
title: newTitle,
|
|
1211
|
+
docId: generateNodeId(),
|
|
1212
|
+
...(resolveTypeMeta(tgtType) || {}),
|
|
1213
|
+
masterDocId: opts.masterDocId,
|
|
1214
|
+
variantType: tgtType,
|
|
1215
|
+
};
|
|
1216
|
+
setActiveDocument(bodyDoc, newTitle, filePath, false, undefined, metadata);
|
|
1217
|
+
const { markdown } = tiptapToMarkdownChecked(bodyDoc, newTitle, metadata);
|
|
1218
|
+
ensureDataDir();
|
|
1219
|
+
atomicWriteFileSync(filePath, markdown);
|
|
1220
|
+
const newFilename = filePath.split(/[/\\]/).pop();
|
|
1221
|
+
return { document: getDocument(), title: getTitle(), filename: newFilename };
|
|
1222
|
+
}
|
|
1151
1223
|
export function getActiveFilename() {
|
|
1152
1224
|
const filePath = getFilePath();
|
|
1153
1225
|
// For external docs, return the full path as the identifier
|
package/dist/server/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
|
14
14
|
import { save, cancelDebouncedSave, load, getDocument, getTitle, getFilePath, getDocId, getMetadata, getStatus, updateDocument, setMetadata, applyTextEdits, isAgentLocked, getPendingDocInfo, getDocTagsByFilename, addDocTag, removeDocTag, markAllNodesAsPending, updatePendingCacheForActiveDoc, removePendingCacheEntry, clearAllCaches, stripPendingAttrs, stripPendingAttrsFromFile, setAutoAcceptOnFile, setSortRequestOnFile, clearSortRequestOnFile, bumpDocVersion, markAsAgentStub, extractText } from './state.js';
|
|
15
15
|
import { syncPostHistory } from './post-sync.js';
|
|
16
16
|
import { enrollManualPostForAutoplug } from './autoplug-enroll.js';
|
|
17
|
-
import { listDocuments, switchDocument, createDocument, deleteDocument, duplicateDocument, reloadDocument, updateDocumentTitle, openFile, reorderDocs, searchDocuments, listArchivedDocuments, archiveDocument, unarchiveDocument, getActiveFilename, batchResolve, listPendingSorts } from './documents.js';
|
|
17
|
+
import { listDocuments, switchDocument, createDocument, deleteDocument, duplicateDocument, createVariant, reloadDocument, updateDocumentTitle, openFile, reorderDocs, searchDocuments, listArchivedDocuments, archiveDocument, unarchiveDocument, getActiveFilename, batchResolve, listPendingSorts } from './documents.js';
|
|
18
18
|
import { writePromptDebug, isPromptDebugEnabled } from './prompt-debug.js';
|
|
19
19
|
import { createWorkspaceRouter } from './workspace-routes.js';
|
|
20
20
|
import { createLinkRouter } from './link-routes.js';
|
|
@@ -638,6 +638,25 @@ export async function startHttpServer(options = {}) {
|
|
|
638
638
|
res.status(400).json({ error: err.message });
|
|
639
639
|
}
|
|
640
640
|
});
|
|
641
|
+
// Create variant: a retyped derivative nested under the master. Field-projection
|
|
642
|
+
// (body always; title folds into body on downcast; target type scaffolded) —
|
|
643
|
+
// NOT a verbatim clone (that's /duplicate). adr: docs/variants.md
|
|
644
|
+
app.post('/api/documents/variant', (req, res) => {
|
|
645
|
+
try {
|
|
646
|
+
const { filename, masterDocId, variantType } = req.body;
|
|
647
|
+
if (!filename || !masterDocId || !variantType) {
|
|
648
|
+
res.status(400).json({ error: 'filename, masterDocId, and variantType are required' });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const result = createVariant(filename, { masterDocId, variantType });
|
|
652
|
+
broadcastDocumentSwitched(result.document, result.title, result.filename);
|
|
653
|
+
broadcastDocumentsChanged();
|
|
654
|
+
res.json(result);
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
res.status(400).json({ error: err.message });
|
|
658
|
+
}
|
|
659
|
+
});
|
|
641
660
|
app.post('/api/documents/batch-resolve', (req, res) => {
|
|
642
661
|
try {
|
|
643
662
|
const { filenames, action } = req.body;
|
package/dist/server/mcp.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getDocument, getWordCount, getPendingChangeCount, getTitle, getStatus,
|
|
|
14
14
|
import { tiptapToBlocks } from './node-blocks.js';
|
|
15
15
|
import { outline, peek, searchInDoc, truncateRead } from './peek-outline.js';
|
|
16
16
|
import { harvestSentenceHashes, harvestCharCount } from './enrichment.js';
|
|
17
|
+
import { resolveTypeMeta } from './content-type-meta.js';
|
|
17
18
|
import { listDocuments, switchDocument, createDocument, createDocumentFile, deleteDocument, openFile, getActiveFilename, promoteTempFile, archiveDocument, unarchiveDocument, resolveDocId, filenameByDocId, searchDocuments, listDirtyDocs, crawlDocs, enrichmentFooter, buildEnrichmentInstructions, listPendingSorts, sortFooter, buildSortInstructions, stagePendingTitle } from './documents.js';
|
|
18
19
|
import { readFrontmatter, writeFrontmatter, computeBacklinksFor, invalidateBacklinksCache } from './backlinks.js';
|
|
19
20
|
import { logger, generateRequestId, withRequestId } from './logger.js';
|
|
@@ -29,19 +30,6 @@ import { tiptapToMarkdown, splitFusedParagraphs } from './markdown.js';
|
|
|
29
30
|
import { loadDocFromDisk } from './pending-overlay.js';
|
|
30
31
|
import { getComments, getCommentCount, getGlobalCommentSummary, resolveComments } from './comments.js';
|
|
31
32
|
import { readTasks, addTask, updateTask, removeTask } from './tasks.js';
|
|
32
|
-
/** Map a content type string to its frontmatter metadata object. */
|
|
33
|
-
function resolveTypeMeta(type, url) {
|
|
34
|
-
switch (type) {
|
|
35
|
-
case 'tweet': return { content_type: 'tweet', tweetContext: { mode: 'tweet' } };
|
|
36
|
-
case 'reply': return { content_type: 'reply', tweetContext: { mode: 'reply', ...(url ? { url } : {}) } };
|
|
37
|
-
case 'quote': return { content_type: 'quote', tweetContext: { mode: 'quote', ...(url ? { url } : {}) } };
|
|
38
|
-
case 'article': return { content_type: 'article', articleContext: { active: true } };
|
|
39
|
-
case 'linkedin': return { content_type: 'linkedin', linkedinContext: { active: true } };
|
|
40
|
-
case 'newsletter': return { content_type: 'newsletter', newsletterContext: { active: true } };
|
|
41
|
-
case 'blog': return { content_type: 'blog', blogContext: { active: true } };
|
|
42
|
-
default: return undefined;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
33
|
/** Resolve a docId to a full document target. Fast path for active doc (zero I/O). */
|
|
46
34
|
function resolveDocTarget(docId) {
|
|
47
35
|
const filename = resolveDocId(docId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openwriter",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.1",
|
|
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",
|