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.
@@ -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
- addDoc(wsTarget.wsFilename, wsTarget.containerId, result.filename, result.title);
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
- addDoc(wsTarget.wsFilename, wsTarget.containerId, result.filename, result.title);
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
- addDoc(wsTarget.wsFilename, wsTarget.containerId, result.filename, result.title);
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
- const result = addContainerToWorkspace(workspaceFile, parentContainerId ?? null, name);
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
- target.unshift(doc);
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
- target.unshift(container);
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.17.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",