openwriter 0.17.0 → 0.18.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/server/mcp.js +18 -7
- package/dist/server/workspace-tree.js +18 -3
- package/dist/server/workspaces.js +2 -2
- package/package.json +1 -1
package/dist/server/mcp.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getDataDir, ensureDataDir, resolveDocPath, generateNodeId, atomicWriteF
|
|
|
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, extractText, countPending, addDocTag, removeDocTag, getCachedDocument, invalidateDocCache, isAutoAcceptActive, removePendingCacheEntry, getExternalMtimeDrift, reloadActiveDocFromDisk, getCanonical, cloneWithPendingReverted, bumpDocVersion, } from './state.js';
|
|
14
14
|
import { tiptapToBlocks } from './node-blocks.js';
|
|
15
15
|
import { harvestSentenceHashes, harvestCharCount } from './enrichment.js';
|
|
16
|
-
import { listDocuments, switchDocument, createDocument, createDocumentFile, deleteDocument, openFile, getActiveFilename, updateDocumentTitle, promoteTempFile, archiveDocument, unarchiveDocument, resolveDocId, searchDocuments, listDirtyDocs, crawlDocs, enrichmentFooter, buildEnrichmentInstructions } from './documents.js';
|
|
16
|
+
import { listDocuments, switchDocument, createDocument, createDocumentFile, deleteDocument, openFile, getActiveFilename, updateDocumentTitle, promoteTempFile, archiveDocument, unarchiveDocument, resolveDocId, filenameByDocId, searchDocuments, listDirtyDocs, crawlDocs, enrichmentFooter, buildEnrichmentInstructions } from './documents.js';
|
|
17
17
|
import { extractForwardLinks } from './backlinks.js';
|
|
18
18
|
import { logger, generateRequestId, withRequestId } from './logger.js';
|
|
19
19
|
import { broadcastDocumentSwitched, broadcastDocumentsChanged, broadcastWorkspacesChanged, broadcastTitleChanged, broadcastMetadataChanged, broadcastPendingDocsChanged, broadcastWritingStarted, broadcastWritingFinished, broadcastCommentsChanged } from './ws.js';
|
|
@@ -414,8 +414,9 @@ export const TOOL_REGISTRY = [
|
|
|
414
414
|
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.'),
|
|
415
415
|
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.'),
|
|
416
416
|
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.'),
|
|
417
|
+
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.'),
|
|
417
418
|
},
|
|
418
|
-
handler: async ({ title, path, workspace, container, empty, content_type, url }) => {
|
|
419
|
+
handler: async ({ title, path, workspace, container, empty, content_type, url, afterId }) => {
|
|
419
420
|
// Require url for reply/quote
|
|
420
421
|
if ((content_type === 'reply' || content_type === 'quote') && !url) {
|
|
421
422
|
return { content: [{ type: 'text', text: `Error: content_type "${content_type}" requires a url parameter (e.g. "https://x.com/user/status/123").` }] };
|
|
@@ -457,7 +458,10 @@ export const TOOL_REGISTRY = [
|
|
|
457
458
|
}
|
|
458
459
|
let wsInfo = '';
|
|
459
460
|
if (wsTarget) {
|
|
460
|
-
|
|
461
|
+
// Resolve afterId: it may be a docId (8-char hex) or containerId.
|
|
462
|
+
// filenameByDocId resolves docId→filename; if null, treat as containerId.
|
|
463
|
+
const afterRef = afterId ? (filenameByDocId(afterId) ?? afterId) : null;
|
|
464
|
+
addDoc(wsTarget.wsFilename, wsTarget.containerId, result.filename, result.title, afterRef);
|
|
461
465
|
wsInfo = ` → workspace "${workspace}"${container ? ` / ${container}` : ''}`;
|
|
462
466
|
}
|
|
463
467
|
const newDocId = getDocId();
|
|
@@ -478,7 +482,8 @@ export const TOOL_REGISTRY = [
|
|
|
478
482
|
const result = createDocumentFile(title, path, typeMeta);
|
|
479
483
|
let wsInfo = '';
|
|
480
484
|
if (wsTarget) {
|
|
481
|
-
|
|
485
|
+
const afterRef = afterId ? (filenameByDocId(afterId) ?? afterId) : null;
|
|
486
|
+
addDoc(wsTarget.wsFilename, wsTarget.containerId, result.filename, result.title, afterRef);
|
|
482
487
|
wsInfo = ` → workspace "${workspace}"${container ? ` / ${container}` : ''}`;
|
|
483
488
|
}
|
|
484
489
|
// Broadcast spinner keyed by filename so populate_document can clear exactly
|
|
@@ -586,6 +591,7 @@ export const TOOL_REGISTRY = [
|
|
|
586
591
|
container: z.string().optional().describe('Container name within the workspace (e.g. "Chapters"). Requires workspace.'),
|
|
587
592
|
url: z.string().optional().describe('Tweet URL — REQUIRED for content_type "reply" or "quote".'),
|
|
588
593
|
path: z.string().optional().describe('Absolute file path to create the document at. If omitted, creates in ~/.openwriter/.'),
|
|
594
|
+
afterId: z.string().optional().describe('Place the new doc immediately after this docId or containerId inside its parent. Omit to append to the bottom (default, ascending-order convention). Requires workspace.'),
|
|
589
595
|
})).min(1).describe('List of documents to declare (minimum 1).'),
|
|
590
596
|
},
|
|
591
597
|
handler: async ({ writes }) => {
|
|
@@ -612,7 +618,8 @@ export const TOOL_REGISTRY = [
|
|
|
612
618
|
const typeMeta = resolveTypeMeta(w.content_type, w.url);
|
|
613
619
|
const result = createDocumentFile(w.title, w.path, typeMeta);
|
|
614
620
|
if (wsTarget) {
|
|
615
|
-
|
|
621
|
+
const afterRef = w.afterId ? (filenameByDocId(w.afterId) ?? w.afterId) : null;
|
|
622
|
+
addDoc(wsTarget.wsFilename, wsTarget.containerId, result.filename, result.title, afterRef);
|
|
616
623
|
}
|
|
617
624
|
broadcastWritingStarted(w.title, wsTarget, result.filename);
|
|
618
625
|
broadcastedKeys.push(result.filename);
|
|
@@ -1090,9 +1097,13 @@ export const TOOL_REGISTRY = [
|
|
|
1090
1097
|
workspaceFile: z.string().describe('Workspace manifest filename'),
|
|
1091
1098
|
name: z.string().describe('Container name (e.g. "Chapters", "Research")'),
|
|
1092
1099
|
parentContainerId: z.string().optional().describe('Parent container ID for nesting (null = root level)'),
|
|
1100
|
+
afterId: z.string().optional().describe('Place the new container 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).'),
|
|
1093
1101
|
},
|
|
1094
|
-
handler: async ({ workspaceFile, name, parentContainerId }) => {
|
|
1095
|
-
|
|
1102
|
+
handler: async ({ workspaceFile, name, parentContainerId, afterId }) => {
|
|
1103
|
+
// Resolve afterId: may be docId (8-char hex) or containerId. filenameByDocId
|
|
1104
|
+
// resolves docId→filename; if null, treat as containerId.
|
|
1105
|
+
const afterRef = afterId ? (filenameByDocId(afterId) ?? afterId) : null;
|
|
1106
|
+
const result = addContainerToWorkspace(workspaceFile, parentContainerId ?? null, name, afterRef);
|
|
1096
1107
|
broadcastWorkspacesChanged();
|
|
1097
1108
|
return { content: [{ type: 'text', text: `Created container "${name}" (id:${result.containerId})` }] };
|
|
1098
1109
|
},
|
|
@@ -89,10 +89,13 @@ export function addDocToContainer(root, containerId, file, title, afterIdentifie
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
else {
|
|
92
|
-
|
|
92
|
+
// Default: append to the bottom of the parent's child list. This matches
|
|
93
|
+
// the ascending-order convention (newest at bottom, oldest at top). Callers
|
|
94
|
+
// that want top-insertion must pass an explicit afterIdentifier.
|
|
95
|
+
target.push(doc);
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
|
-
export function addContainer(root, parentContainerId, name) {
|
|
98
|
+
export function addContainer(root, parentContainerId, name, afterIdentifier) {
|
|
96
99
|
const depth = getContainerDepth(root, parentContainerId);
|
|
97
100
|
if (depth >= MAX_DEPTH) {
|
|
98
101
|
throw new Error(`Maximum nesting depth (${MAX_DEPTH}) reached`);
|
|
@@ -106,7 +109,19 @@ export function addContainer(root, parentContainerId, name) {
|
|
|
106
109
|
name,
|
|
107
110
|
items: [],
|
|
108
111
|
};
|
|
109
|
-
|
|
112
|
+
if (afterIdentifier) {
|
|
113
|
+
const afterIdx = target.findIndex((n) => (n.type === 'doc' && n.file === afterIdentifier) || (n.type === 'container' && n.id === afterIdentifier));
|
|
114
|
+
if (afterIdx === -1) {
|
|
115
|
+
target.push(container);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
target.splice(afterIdx + 1, 0, container);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// Default: append to the bottom (ascending-order convention).
|
|
123
|
+
target.push(container);
|
|
124
|
+
}
|
|
110
125
|
return container;
|
|
111
126
|
}
|
|
112
127
|
// ============================================================================
|
|
@@ -210,9 +210,9 @@ export function reorderDoc(wsFile, file, afterFile) {
|
|
|
210
210
|
// ============================================================================
|
|
211
211
|
// CONTAINER OPERATIONS
|
|
212
212
|
// ============================================================================
|
|
213
|
-
export function addContainerToWorkspace(wsFile, parentContainerId, name) {
|
|
213
|
+
export function addContainerToWorkspace(wsFile, parentContainerId, name, afterIdentifier) {
|
|
214
214
|
const ws = getWorkspace(wsFile);
|
|
215
|
-
const container = addContainerToTree(ws.root, parentContainerId, name);
|
|
215
|
+
const container = addContainerToTree(ws.root, parentContainerId, name, afterIdentifier ?? null);
|
|
216
216
|
writeWorkspace(wsFile, ws);
|
|
217
217
|
return { workspace: ws, containerId: container.id };
|
|
218
218
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openwriter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|