openwriter 0.35.1 → 0.36.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.
Files changed (32) hide show
  1. package/dist/client/assets/{index-Be_l2OOL.css → index-B5p6e-z0.css} +1 -1
  2. package/dist/client/assets/{index-BPDt3Psd.js → index-BMhKsQ_t.js} +53 -53
  3. package/dist/client/index.html +2 -2
  4. package/dist/plugins/authors-voice/skill/LICENSE +21 -0
  5. package/dist/plugins/authors-voice/skill/README.md +126 -0
  6. package/dist/plugins/authors-voice/skill/SKILL.md +151 -0
  7. package/dist/plugins/authors-voice/skill/catalog/ai-tells.md +144 -0
  8. package/dist/plugins/authors-voice/skill/catalog/anchor-prompt.md +189 -0
  9. package/dist/plugins/authors-voice/skill/catalog/author-hints.md +119 -0
  10. package/dist/plugins/authors-voice/skill/catalog/fingerprints.md +175 -0
  11. package/dist/plugins/authors-voice/skill/catalog/hurdle.md +76 -0
  12. package/dist/plugins/authors-voice/skill/catalog/post-write-audit.md +105 -0
  13. package/dist/plugins/authors-voice/skill/docs/analysis.md +31 -0
  14. package/dist/plugins/authors-voice/skill/docs/anchor-iteration.md +176 -0
  15. package/dist/plugins/authors-voice/skill/docs/api/import.md +78 -0
  16. package/dist/plugins/authors-voice/skill/docs/api/protocol.md +140 -0
  17. package/dist/plugins/authors-voice/skill/docs/api/setup.md +37 -0
  18. package/dist/plugins/authors-voice/skill/docs/api/tools.md +102 -0
  19. package/dist/plugins/authors-voice/skill/docs/api/troubleshooting.md +7 -0
  20. package/dist/plugins/authors-voice/skill/docs/apply-protocol-deep.md +191 -0
  21. package/dist/plugins/authors-voice/skill/docs/context-hygiene.md +33 -0
  22. package/dist/plugins/authors-voice/skill/docs/setup.md +74 -0
  23. package/dist/plugins/authors-voice/skill/docs/tiers.md +13 -0
  24. package/dist/plugins/authors-voice/skill/package.json +35 -0
  25. package/dist/plugins/authors-voice/skill/prompts/skeleton.md +29 -0
  26. package/dist/plugins/authors-voice/skill/voice/README.md +51 -0
  27. package/dist/plugins/authors-voice/skill/voice/corpus/.gitkeep +0 -0
  28. package/dist/server/documents.js +7 -10
  29. package/dist/server/state.js +27 -7
  30. package/dist/server/title-resolve.js +87 -0
  31. package/dist/server/workspaces.js +10 -4
  32. package/package.json +1 -1
@@ -1596,7 +1596,27 @@ export function setPendingCacheEntry(filename, count) {
1596
1596
  pendingDocCache.delete(filename);
1597
1597
  }
1598
1598
  }
1599
- /** Populate the pending cache from a full disk scan. Called once on startup. */
1599
+ /** Pending count for one doc: sidecar entries + staged title rename (the
1600
+ * sidecar is authoritative since the overlay migration; legacy in-frontmatter
1601
+ * `pending:` is the fallback for docs that predate it).
1602
+ * adr: adr/pending-overlay-model.md */
1603
+ function pendingCountForDoc(docId, legacyPending) {
1604
+ let count = 0;
1605
+ if (docId) {
1606
+ count += loadOverlay(docId).length;
1607
+ if (loadPendingMetadata(docId))
1608
+ count += 1;
1609
+ }
1610
+ if (count === 0 && legacyPending && Object.keys(legacyPending).length > 0) {
1611
+ count = Object.keys(legacyPending).length;
1612
+ }
1613
+ return count;
1614
+ }
1615
+ /** Populate the pending cache from a full disk scan. Called once on startup.
1616
+ * Reads the `_pending/{docId}.json` sidecars (the authoritative store) —
1617
+ * scanning only legacy frontmatter here made every restart look like a
1618
+ * profile-wide accept-all, because the sidebar/review pending list came back
1619
+ * empty even though all sidecars survived on disk. */
1600
1620
  function populatePendingCache() {
1601
1621
  pendingDocCache.clear();
1602
1622
  try {
@@ -1605,9 +1625,9 @@ function populatePendingCache() {
1605
1625
  try {
1606
1626
  const raw = readFileSync(join(getDataDir(), f), 'utf-8');
1607
1627
  const { data } = matter(raw);
1608
- if (data.pending && Object.keys(data.pending).length > 0) {
1609
- pendingDocCache.set(f, Object.keys(data.pending).length);
1610
- }
1628
+ const count = pendingCountForDoc(data.docId, data.pending);
1629
+ if (count > 0)
1630
+ pendingDocCache.set(f, count);
1611
1631
  }
1612
1632
  catch { /* skip unreadable files */ }
1613
1633
  }
@@ -1620,9 +1640,9 @@ function populatePendingCache() {
1620
1640
  continue;
1621
1641
  const raw = readFileSync(extPath, 'utf-8');
1622
1642
  const { data } = matter(raw);
1623
- if (data.pending && Object.keys(data.pending).length > 0) {
1624
- pendingDocCache.set(extPath, Object.keys(data.pending).length);
1625
- }
1643
+ const count = pendingCountForDoc(data.docId, data.pending);
1644
+ if (count > 0)
1645
+ pendingDocCache.set(extPath, count);
1626
1646
  }
1627
1647
  catch { /* skip unreadable files */ }
1628
1648
  }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Listing-time title resolution for documents that haven't been opened yet.
3
+ *
4
+ * Markdown files dropped into the profile directory externally (no
5
+ * frontmatter) used to render as "Untitled" in the sidebar and in MCP
6
+ * list_documents until first opened — opening injects the frontmatter title
7
+ * that listing relied on exclusively. This module fixes the class: title
8
+ * resolution at read time falls back through
9
+ *
10
+ * frontmatter title → workspace JSON entry title → first h1 in body → filename stem
11
+ *
12
+ * This is deliberately fallback-only — no lazy frontmatter injection on the
13
+ * listing path. Listing is a read; writing to every titleless file on index
14
+ * would mutate user-dropped files behind their back (surprising for git and
15
+ * external editors, and racy against an editor mid-write). Frontmatter is
16
+ * still injected on first open/save, exactly as before — at which point the
17
+ * resolved title is what gets persisted.
18
+ */
19
+ import { readFileSync, readdirSync } from 'fs';
20
+ import { join, basename } from 'path';
21
+ import { getWorkspacesDir, TEMP_PREFIX } from './helpers.js';
22
+ function isRealTitle(t) {
23
+ return typeof t === 'string' && t.trim() !== '' && t !== 'Untitled';
24
+ }
25
+ /** First ATX h1 in the body, with trailing closing hashes stripped. */
26
+ export function firstH1(content) {
27
+ const m = content.match(/^#[ \t]+(.+?)[ \t]*#*[ \t]*$/m);
28
+ return m ? m[1].trim() || null : null;
29
+ }
30
+ export function resolveListingTitle(src) {
31
+ if (isRealTitle(src.fmTitle))
32
+ return src.fmTitle;
33
+ if (isRealTitle(src.workspaceTitle))
34
+ return src.workspaceTitle;
35
+ if (src.content) {
36
+ const h1 = firstH1(src.content);
37
+ if (h1)
38
+ return h1;
39
+ }
40
+ if (src.filename) {
41
+ const stem = basename(src.filename).replace(/\.md$/i, '');
42
+ // Temp files are genuinely new/unnamed docs — their generated stem is
43
+ // not a title; keep them "Untitled" so auto-titling still owns them.
44
+ if (stem && !basename(src.filename).startsWith(TEMP_PREFIX))
45
+ return stem;
46
+ }
47
+ return 'Untitled';
48
+ }
49
+ /**
50
+ * Map of doc file identifier → title from workspace JSON entries.
51
+ * Read-only raw scan of _workspaces/*.json — intentionally does NOT go
52
+ * through readWorkspace(), which performs migrations and writes back.
53
+ * First entry wins when a doc appears in multiple workspaces.
54
+ */
55
+ export function getWorkspaceTitleMap() {
56
+ const map = new Map();
57
+ let files = [];
58
+ try {
59
+ files = readdirSync(getWorkspacesDir()).filter((f) => f.endsWith('.json') && !f.startsWith('_'));
60
+ }
61
+ catch {
62
+ return map;
63
+ }
64
+ for (const f of files) {
65
+ try {
66
+ const ws = JSON.parse(readFileSync(join(getWorkspacesDir(), f), 'utf-8'));
67
+ walkNodes(Array.isArray(ws.root) ? ws.root : [], map);
68
+ }
69
+ catch { /* skip unreadable workspace */ }
70
+ }
71
+ return map;
72
+ }
73
+ function walkNodes(nodes, map) {
74
+ for (const node of nodes) {
75
+ if (!node || typeof node !== 'object')
76
+ continue;
77
+ if (node.type === 'doc' && typeof node.file === 'string') {
78
+ if (isRealTitle(node.title) && !map.has(node.file))
79
+ map.set(node.file, node.title);
80
+ if (Array.isArray(node.children))
81
+ walkNodes(node.children, map);
82
+ }
83
+ else if (node.type === 'container' && Array.isArray(node.items)) {
84
+ walkNodes(node.items, map);
85
+ }
86
+ }
87
+ }
@@ -10,6 +10,7 @@ import matter from 'gray-matter';
10
10
  import trash from 'trash';
11
11
  import { getWorkspacesDir, ensureWorkspacesDir, sanitizeFilename, resolveDocPath, isExternalDoc } from './helpers.js';
12
12
  import { markdownToTiptap, tiptapToMarkdown } from './markdown.js';
13
+ import { resolveListingTitle } from './title-resolve.js';
13
14
  function getOrderFile() { return join(getWorkspacesDir(), '_order.json'); }
14
15
  import { isV1, migrateV1toV2 } from './workspace-types.js';
15
16
  import { addDocToContainer, addContainer as addContainerToTree, removeNode, moveNode, reorderNode, findContainer, collectAllFiles, countDocs, findDocNode } from './workspace-tree.js';
@@ -588,10 +589,15 @@ export function collectFilesInContainer(wsFile, containerId) {
588
589
  export function getWorkspaceStructure(filename) {
589
590
  return getWorkspace(filename);
590
591
  }
591
- /** Read the frontmatter title for a doc file. Falls back to filename without extension. */
592
+ /** Read the title for a doc file: frontmatter first h1 in body → filename stem. */
592
593
  export function getDocTitle(filename) {
593
- const fm = readDocFrontmatter(filename);
594
- if (fm?.title && fm.title !== 'Untitled')
595
- return fm.title;
594
+ try {
595
+ const filePath = resolveDocPath(filename);
596
+ if (existsSync(filePath)) {
597
+ const { data, content } = matter(readFileSync(filePath, 'utf-8'));
598
+ return resolveListingTitle({ fmTitle: data.title, content, filename });
599
+ }
600
+ }
601
+ catch { /* fall through to stem */ }
596
602
  return filename.replace(/\.md$/, '');
597
603
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openwriter",
3
- "version": "0.35.1",
3
+ "version": "0.36.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",