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.
- package/dist/client/assets/{index-Be_l2OOL.css → index-B5p6e-z0.css} +1 -1
- package/dist/client/assets/{index-BPDt3Psd.js → index-BMhKsQ_t.js} +53 -53
- package/dist/client/index.html +2 -2
- package/dist/plugins/authors-voice/skill/LICENSE +21 -0
- package/dist/plugins/authors-voice/skill/README.md +126 -0
- package/dist/plugins/authors-voice/skill/SKILL.md +151 -0
- package/dist/plugins/authors-voice/skill/catalog/ai-tells.md +144 -0
- package/dist/plugins/authors-voice/skill/catalog/anchor-prompt.md +189 -0
- package/dist/plugins/authors-voice/skill/catalog/author-hints.md +119 -0
- package/dist/plugins/authors-voice/skill/catalog/fingerprints.md +175 -0
- package/dist/plugins/authors-voice/skill/catalog/hurdle.md +76 -0
- package/dist/plugins/authors-voice/skill/catalog/post-write-audit.md +105 -0
- package/dist/plugins/authors-voice/skill/docs/analysis.md +31 -0
- package/dist/plugins/authors-voice/skill/docs/anchor-iteration.md +176 -0
- package/dist/plugins/authors-voice/skill/docs/api/import.md +78 -0
- package/dist/plugins/authors-voice/skill/docs/api/protocol.md +140 -0
- package/dist/plugins/authors-voice/skill/docs/api/setup.md +37 -0
- package/dist/plugins/authors-voice/skill/docs/api/tools.md +102 -0
- package/dist/plugins/authors-voice/skill/docs/api/troubleshooting.md +7 -0
- package/dist/plugins/authors-voice/skill/docs/apply-protocol-deep.md +191 -0
- package/dist/plugins/authors-voice/skill/docs/context-hygiene.md +33 -0
- package/dist/plugins/authors-voice/skill/docs/setup.md +74 -0
- package/dist/plugins/authors-voice/skill/docs/tiers.md +13 -0
- package/dist/plugins/authors-voice/skill/package.json +35 -0
- package/dist/plugins/authors-voice/skill/prompts/skeleton.md +29 -0
- package/dist/plugins/authors-voice/skill/voice/README.md +51 -0
- package/dist/plugins/authors-voice/skill/voice/corpus/.gitkeep +0 -0
- package/dist/server/documents.js +7 -10
- package/dist/server/state.js +27 -7
- package/dist/server/title-resolve.js +87 -0
- package/dist/server/workspaces.js +10 -4
- package/package.json +1 -1
package/dist/server/state.js
CHANGED
|
@@ -1596,7 +1596,27 @@ export function setPendingCacheEntry(filename, count) {
|
|
|
1596
1596
|
pendingDocCache.delete(filename);
|
|
1597
1597
|
}
|
|
1598
1598
|
}
|
|
1599
|
-
/**
|
|
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
|
-
|
|
1609
|
-
|
|
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
|
-
|
|
1624
|
-
|
|
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
|
|
592
|
+
/** Read the title for a doc file: frontmatter → first h1 in body → filename stem. */
|
|
592
593
|
export function getDocTitle(filename) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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.
|
|
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",
|