openwriter 0.40.0 → 0.40.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.
Files changed (62) hide show
  1. package/dist/client/assets/{index-vmEPerKn.js → index-Dxbv2n2m.js} +1 -1
  2. package/dist/client/index.html +1 -1
  3. package/package.json +1 -1
  4. package/dist/plugins/authors-voice/dist/index.d.ts +0 -48
  5. package/dist/plugins/authors-voice/dist/index.js +0 -235
  6. package/dist/plugins/authors-voice/package.json +0 -24
  7. package/dist/plugins/authors-voice/skill/LICENSE +0 -21
  8. package/dist/plugins/authors-voice/skill/README.md +0 -126
  9. package/dist/plugins/authors-voice/skill/SKILL.md +0 -151
  10. package/dist/plugins/authors-voice/skill/catalog/ai-tells.md +0 -144
  11. package/dist/plugins/authors-voice/skill/catalog/anchor-prompt.md +0 -189
  12. package/dist/plugins/authors-voice/skill/catalog/author-hints.md +0 -119
  13. package/dist/plugins/authors-voice/skill/catalog/fingerprints.md +0 -175
  14. package/dist/plugins/authors-voice/skill/catalog/hurdle.md +0 -76
  15. package/dist/plugins/authors-voice/skill/catalog/post-write-audit.md +0 -105
  16. package/dist/plugins/authors-voice/skill/docs/analysis.md +0 -31
  17. package/dist/plugins/authors-voice/skill/docs/anchor-iteration.md +0 -176
  18. package/dist/plugins/authors-voice/skill/docs/api/import.md +0 -78
  19. package/dist/plugins/authors-voice/skill/docs/api/protocol.md +0 -140
  20. package/dist/plugins/authors-voice/skill/docs/api/setup.md +0 -37
  21. package/dist/plugins/authors-voice/skill/docs/api/tools.md +0 -102
  22. package/dist/plugins/authors-voice/skill/docs/api/troubleshooting.md +0 -7
  23. package/dist/plugins/authors-voice/skill/docs/apply-protocol-deep.md +0 -191
  24. package/dist/plugins/authors-voice/skill/docs/context-hygiene.md +0 -33
  25. package/dist/plugins/authors-voice/skill/docs/setup.md +0 -74
  26. package/dist/plugins/authors-voice/skill/docs/tiers.md +0 -13
  27. package/dist/plugins/authors-voice/skill/package.json +0 -35
  28. package/dist/plugins/authors-voice/skill/prompts/skeleton.md +0 -29
  29. package/dist/plugins/authors-voice/skill/voice/README.md +0 -51
  30. package/dist/plugins/authors-voice/skill/voice/corpus/.gitkeep +0 -0
  31. package/dist/plugins/github/dist/blog-tools.d.ts +0 -65
  32. package/dist/plugins/github/dist/blog-tools.js +0 -1189
  33. package/dist/plugins/github/dist/git-sync.d.ts +0 -46
  34. package/dist/plugins/github/dist/git-sync.js +0 -335
  35. package/dist/plugins/github/dist/helpers.d.ts +0 -127
  36. package/dist/plugins/github/dist/helpers.js +0 -67
  37. package/dist/plugins/github/dist/index.d.ts +0 -12
  38. package/dist/plugins/github/dist/index.js +0 -112
  39. package/dist/plugins/github/package.json +0 -24
  40. package/dist/plugins/image-gen/dist/index.d.ts +0 -35
  41. package/dist/plugins/image-gen/dist/index.js +0 -149
  42. package/dist/plugins/image-gen/package.json +0 -26
  43. package/dist/plugins/publish/dist/helpers.d.ts +0 -66
  44. package/dist/plugins/publish/dist/helpers.js +0 -199
  45. package/dist/plugins/publish/dist/index.d.ts +0 -3
  46. package/dist/plugins/publish/dist/index.js +0 -1156
  47. package/dist/plugins/publish/dist/newsletter-tools.d.ts +0 -2
  48. package/dist/plugins/publish/dist/newsletter-tools.js +0 -394
  49. package/dist/plugins/publish/package.json +0 -31
  50. package/dist/plugins/x-api/dist/index.d.ts +0 -27
  51. package/dist/plugins/x-api/dist/index.js +0 -368
  52. package/dist/plugins/x-api/dist/server-bridge.d.ts +0 -22
  53. package/dist/plugins/x-api/dist/server-bridge.js +0 -43
  54. package/dist/plugins/x-api/package.json +0 -27
  55. package/dist/server/blog-routes.js +0 -160
  56. package/dist/server/git-sync.js +0 -273
  57. package/dist/server/marks.js +0 -182
  58. package/dist/server/sync-routes.js +0 -75
  59. package/skill/docs/enrichment.md +0 -180
  60. package/skill/docs/footnotes.md +0 -178
  61. package/skill/docs/setup.md +0 -62
  62. package/skill/docs/welcome.md +0 -21
@@ -1,273 +0,0 @@
1
- /**
2
- * Git sync module: all git/gh CLI interactions for GitHub sync.
3
- * Uses child_process.execFile with shell:true (required on Windows).
4
- */
5
- import { execFile } from 'child_process';
6
- import { existsSync, writeFileSync } from 'fs';
7
- import { join } from 'path';
8
- import { getDataDir, readConfig, saveConfig } from './helpers.js';
9
- import { save, cancelDebouncedSave } from './state.js';
10
- const GITIGNORE_CONTENT = `config.json\n.versions/\n`;
11
- const NETWORK_TIMEOUT = 30000;
12
- let currentSyncState = 'unconfigured';
13
- let lastError;
14
- function exec(cmd, args, cwd, timeout = 10000) {
15
- // Quote args with spaces so shell: true doesn't split them
16
- const safeArgs = args.map(a => a.includes(' ') ? `"${a}"` : a);
17
- return new Promise((resolve, reject) => {
18
- execFile(cmd, safeArgs, { cwd, shell: true, timeout }, (err, stdout, stderr) => {
19
- if (err)
20
- reject(new Error(stderr?.trim() || err.message));
21
- else
22
- resolve(stdout.trim());
23
- });
24
- });
25
- }
26
- export async function isGitInstalled() {
27
- try {
28
- await exec('git', ['--version'], getDataDir());
29
- return true;
30
- }
31
- catch {
32
- return false;
33
- }
34
- }
35
- export async function isGhInstalled() {
36
- try {
37
- await exec('gh', ['--version'], getDataDir());
38
- return true;
39
- }
40
- catch {
41
- return false;
42
- }
43
- }
44
- export async function isGhAuthenticated() {
45
- try {
46
- await exec('gh', ['auth', 'status'], getDataDir());
47
- return true;
48
- }
49
- catch {
50
- return false;
51
- }
52
- }
53
- export function isGitRepo() {
54
- return existsSync(join(getDataDir(), '.git'));
55
- }
56
- function ensureGitignore() {
57
- const gitignorePath = join(getDataDir(), '.gitignore');
58
- if (!existsSync(gitignorePath)) {
59
- writeFileSync(gitignorePath, GITIGNORE_CONTENT, 'utf-8');
60
- }
61
- }
62
- /** Count files that have changed since last commit (or all files if no commits yet). */
63
- async function countPendingFiles() {
64
- if (!isGitRepo())
65
- return 0;
66
- try {
67
- // Check for any changes (staged + unstaged + untracked)
68
- const status = await exec('git', ['status', '--porcelain'], getDataDir());
69
- if (!status)
70
- return 0;
71
- return status.split('\n').filter(Boolean).length;
72
- }
73
- catch {
74
- return 0;
75
- }
76
- }
77
- /** Return the list of pending files with their status. */
78
- export async function getPendingFiles() {
79
- if (!isGitRepo())
80
- return [];
81
- try {
82
- const output = await exec('git', ['status', '--porcelain'], getDataDir());
83
- if (!output)
84
- return [];
85
- return output.split('\n').filter(Boolean).map(line => {
86
- const code = line.substring(0, 2);
87
- const file = line.substring(3);
88
- let status = 'modified';
89
- if (code.includes('?') || code.includes('A'))
90
- status = 'added';
91
- else if (code.includes('D'))
92
- status = 'deleted';
93
- else if (code.includes('R'))
94
- status = 'renamed';
95
- return { status, file };
96
- });
97
- }
98
- catch {
99
- return [];
100
- }
101
- }
102
- export async function getSyncStatus() {
103
- const config = readConfig();
104
- if (!config.gitConfigured || !isGitRepo()) {
105
- return { state: 'unconfigured' };
106
- }
107
- if (currentSyncState === 'syncing') {
108
- return { state: 'syncing' };
109
- }
110
- if (currentSyncState === 'error' && lastError) {
111
- return { state: 'error', error: lastError, lastSyncTime: config.lastSyncTime };
112
- }
113
- const pending = await countPendingFiles();
114
- return {
115
- state: pending > 0 ? 'pending' : 'synced',
116
- pendingFiles: pending,
117
- lastSyncTime: config.lastSyncTime,
118
- };
119
- }
120
- export async function getCapabilities() {
121
- const [git, gh] = await Promise.all([isGitInstalled(), isGhInstalled()]);
122
- let ghAuth = false;
123
- if (gh)
124
- ghAuth = await isGhAuthenticated();
125
- let remoteUrl;
126
- if (isGitRepo()) {
127
- try {
128
- remoteUrl = await exec('git', ['remote', 'get-url', 'origin'], getDataDir());
129
- }
130
- catch { /* no remote */ }
131
- }
132
- return {
133
- gitInstalled: git,
134
- ghInstalled: gh,
135
- ghAuthenticated: ghAuth,
136
- existingRepo: isGitRepo(),
137
- remoteUrl,
138
- };
139
- }
140
- async function initRepo() {
141
- if (!isGitRepo()) {
142
- await exec('git', ['init'], getDataDir());
143
- }
144
- ensureGitignore();
145
- // Ensure git user is configured (required for commits)
146
- try {
147
- await exec('git', ['config', 'user.name'], getDataDir());
148
- }
149
- catch {
150
- await exec('git', ['config', 'user.name', 'OpenWriter'], getDataDir());
151
- }
152
- try {
153
- await exec('git', ['config', 'user.email'], getDataDir());
154
- }
155
- catch {
156
- await exec('git', ['config', 'user.email', 'openwriter@local'], getDataDir());
157
- }
158
- }
159
- async function initialCommit() {
160
- await exec('git', ['add', '-A'], getDataDir());
161
- // Check if there's anything staged
162
- const status = await exec('git', ['status', '--porcelain'], getDataDir());
163
- if (!status)
164
- return; // Nothing to commit
165
- await exec('git', ['commit', '-m', 'Initial sync from OpenWriter'], getDataDir());
166
- // Ensure branch is named 'main'
167
- await exec('git', ['branch', '-M', 'main'], getDataDir());
168
- }
169
- export async function setupWithGh(repoName, isPrivate) {
170
- await initRepo();
171
- await initialCommit();
172
- const visibility = isPrivate ? '--private' : '--public';
173
- // Create repo without --push, then push separately for better error control
174
- await exec('gh', ['repo', 'create', repoName, visibility, '--source=.', '--remote=origin'], getDataDir(), NETWORK_TIMEOUT);
175
- await exec('git', ['push', '-u', 'origin', 'main'], getDataDir(), NETWORK_TIMEOUT);
176
- saveConfig({
177
- gitConfigured: true,
178
- repoName,
179
- lastSyncTime: new Date().toISOString(),
180
- });
181
- currentSyncState = 'synced';
182
- }
183
- export async function setupWithPat(pat, repoName, isPrivate) {
184
- // Create repo via GitHub REST API
185
- const res = await fetch('https://api.github.com/user/repos', {
186
- method: 'POST',
187
- headers: {
188
- Authorization: `Bearer ${pat}`,
189
- 'Content-Type': 'application/json',
190
- Accept: 'application/vnd.github+json',
191
- },
192
- body: JSON.stringify({ name: repoName, private: isPrivate, auto_init: false }),
193
- });
194
- if (!res.ok) {
195
- const body = await res.json().catch(() => ({}));
196
- throw new Error(body.message || `GitHub API error: ${res.status}`);
197
- }
198
- const repo = await res.json();
199
- const remoteUrl = `https://${pat}@github.com/${repo.full_name}.git`;
200
- await initRepo();
201
- await initialCommit();
202
- // Set remote
203
- try {
204
- await exec('git', ['remote', 'remove', 'origin'], getDataDir());
205
- }
206
- catch { /* no remote */ }
207
- await exec('git', ['remote', 'add', 'origin', remoteUrl], getDataDir());
208
- await exec('git', ['push', '-u', 'origin', 'main'], getDataDir(), NETWORK_TIMEOUT);
209
- saveConfig({
210
- gitConfigured: true,
211
- gitPat: pat,
212
- repoName,
213
- gitRemote: repo.html_url,
214
- lastSyncTime: new Date().toISOString(),
215
- });
216
- currentSyncState = 'synced';
217
- }
218
- export async function connectExisting(remoteUrl, pat) {
219
- await initRepo();
220
- await initialCommit();
221
- // Embed PAT in URL if provided
222
- let finalUrl = remoteUrl;
223
- if (pat && remoteUrl.startsWith('https://')) {
224
- finalUrl = remoteUrl.replace('https://', `https://${pat}@`);
225
- }
226
- try {
227
- await exec('git', ['remote', 'remove', 'origin'], getDataDir());
228
- }
229
- catch { /* no remote */ }
230
- await exec('git', ['remote', 'add', 'origin', finalUrl], getDataDir());
231
- await exec('git', ['push', '-u', 'origin', 'main'], getDataDir(), NETWORK_TIMEOUT);
232
- saveConfig({
233
- gitConfigured: true,
234
- gitPat: pat,
235
- gitRemote: remoteUrl,
236
- lastSyncTime: new Date().toISOString(),
237
- });
238
- currentSyncState = 'synced';
239
- }
240
- export async function pushSync(onStatus) {
241
- currentSyncState = 'syncing';
242
- lastError = undefined;
243
- onStatus({ state: 'syncing' });
244
- try {
245
- // Flush current document to disk first (cancel debounce to ensure immediate write)
246
- cancelDebouncedSave();
247
- save();
248
- ensureGitignore();
249
- await exec('git', ['add', '-A'], getDataDir());
250
- // Check if there's anything to commit
251
- const status = await exec('git', ['status', '--porcelain'], getDataDir());
252
- if (status) {
253
- const timestamp = new Date().toLocaleString('en-US', {
254
- month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit',
255
- });
256
- await exec('git', ['commit', '-m', `Sync: ${timestamp}`], getDataDir());
257
- }
258
- await exec('git', ['push'], getDataDir(), NETWORK_TIMEOUT);
259
- const now = new Date().toISOString();
260
- saveConfig({ lastSyncTime: now });
261
- currentSyncState = 'synced';
262
- const result = { state: 'synced', lastSyncTime: now, pendingFiles: 0 };
263
- onStatus(result);
264
- return result;
265
- }
266
- catch (err) {
267
- currentSyncState = 'error';
268
- lastError = err.message;
269
- const result = { state: 'error', error: err.message };
270
- onStatus(result);
271
- return result;
272
- }
273
- }
@@ -1,182 +0,0 @@
1
- /**
2
- * Agent Marks: sidecar JSON storage for inline user feedback.
3
- * Each document gets a sidecar file at DATA_DIR/_marks/{filename}.json.
4
- */
5
- import { join } from 'path';
6
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync, renameSync } from 'fs';
7
- import { randomUUID } from 'crypto';
8
- import { getDataDir, ensureDataDir } from './helpers.js';
9
- function getMarksDir() { return join(getDataDir(), '_marks'); }
10
- function ensureMarksDir() {
11
- ensureDataDir();
12
- if (!existsSync(getMarksDir()))
13
- mkdirSync(getMarksDir(), { recursive: true });
14
- }
15
- function markFilePath(filename) {
16
- // Sanitize: replace path separators to avoid nested paths
17
- const safe = filename.replace(/[/\\]/g, '_');
18
- return join(getMarksDir(), `${safe}.json`);
19
- }
20
- function readMarkFile(filename) {
21
- const path = markFilePath(filename);
22
- if (!existsSync(path))
23
- return { marks: [] };
24
- try {
25
- return JSON.parse(readFileSync(path, 'utf-8'));
26
- }
27
- catch {
28
- return { marks: [] };
29
- }
30
- }
31
- function writeMarkFile(filename, data) {
32
- ensureMarksDir();
33
- const path = markFilePath(filename);
34
- if (data.marks.length === 0) {
35
- // Clean up empty sidecar files
36
- if (existsSync(path))
37
- unlinkSync(path);
38
- return;
39
- }
40
- writeFileSync(path, JSON.stringify(data, null, 2));
41
- }
42
- export function addMark(filename, text, note, nodeId, nodeIds) {
43
- const data = readMarkFile(filename);
44
- const mark = {
45
- id: randomUUID().slice(0, 8),
46
- text,
47
- note,
48
- nodeId,
49
- ...(nodeIds && nodeIds.length > 1 ? { nodeIds } : {}),
50
- createdAt: new Date().toISOString(),
51
- };
52
- data.marks.push(mark);
53
- writeMarkFile(filename, data);
54
- return mark;
55
- }
56
- export function getMarks(filename) {
57
- if (filename) {
58
- const data = readMarkFile(filename);
59
- if (data.marks.length === 0)
60
- return {};
61
- return { [filename]: data.marks };
62
- }
63
- // All docs: scan _marks directory
64
- ensureMarksDir();
65
- const result = {};
66
- try {
67
- const files = readdirSync(getMarksDir());
68
- for (const file of files) {
69
- if (!file.endsWith('.json'))
70
- continue;
71
- const docFilename = file.replace(/\.json$/, '').replace(/_/g, ' ');
72
- // Read raw to avoid filename roundtrip issues
73
- const path = join(getMarksDir(), file);
74
- try {
75
- const data = JSON.parse(readFileSync(path, 'utf-8'));
76
- if (data.marks.length > 0)
77
- result[docFilename] = data.marks;
78
- }
79
- catch { /* skip corrupt files */ }
80
- }
81
- }
82
- catch { /* dir doesn't exist yet */ }
83
- return result;
84
- }
85
- export function getMarkCount(filename) {
86
- return readMarkFile(filename).marks.length;
87
- }
88
- /** Count marks across all documents, optionally excluding one filename. */
89
- export function getGlobalMarkSummary(excludeFilename) {
90
- ensureMarksDir();
91
- let totalMarks = 0;
92
- let docCount = 0;
93
- try {
94
- const files = readdirSync(getMarksDir());
95
- for (const file of files) {
96
- if (!file.endsWith('.json'))
97
- continue;
98
- if (excludeFilename) {
99
- const safe = excludeFilename.replace(/[/\\]/g, '_');
100
- if (file === `${safe}.json`)
101
- continue;
102
- }
103
- const path = join(getMarksDir(), file);
104
- try {
105
- const data = JSON.parse(readFileSync(path, 'utf-8'));
106
- if (data.marks.length > 0) {
107
- totalMarks += data.marks.length;
108
- docCount++;
109
- }
110
- }
111
- catch { /* skip */ }
112
- }
113
- }
114
- catch { /* dir doesn't exist */ }
115
- return { totalMarks, docCount };
116
- }
117
- export function editMark(filename, id, note) {
118
- const data = readMarkFile(filename);
119
- const mark = data.marks.find((m) => m.id === id);
120
- if (!mark)
121
- return null;
122
- mark.note = note;
123
- writeMarkFile(filename, data);
124
- return mark;
125
- }
126
- export function resolveMarks(ids) {
127
- const idSet = new Set(ids);
128
- const resolved = [];
129
- ensureMarksDir();
130
- try {
131
- const files = readdirSync(getMarksDir());
132
- for (const file of files) {
133
- if (!file.endsWith('.json'))
134
- continue;
135
- const filePath = join(getMarksDir(), file);
136
- try {
137
- const data = JSON.parse(readFileSync(filePath, 'utf-8'));
138
- const before = data.marks.length;
139
- data.marks = data.marks.filter((m) => {
140
- if (idSet.has(m.id)) {
141
- resolved.push(m.id);
142
- return false;
143
- }
144
- return true;
145
- });
146
- if (data.marks.length !== before) {
147
- const docFilename = file.replace(/\.json$/, '').replace(/_/g, ' ');
148
- writeMarkFile(docFilename, data);
149
- }
150
- }
151
- catch { /* skip */ }
152
- }
153
- }
154
- catch { /* dir doesn't exist */ }
155
- return resolved;
156
- }
157
- export function pruneStaleMarks(filename, validNodeIds) {
158
- const data = readMarkFile(filename);
159
- if (data.marks.length === 0)
160
- return 0;
161
- const validSet = new Set(validNodeIds);
162
- const before = data.marks.length;
163
- data.marks = data.marks.filter((m) => {
164
- // Multi-node mark: keep if ANY nodeId is still valid
165
- if (m.nodeIds && m.nodeIds.length > 0) {
166
- return m.nodeIds.some((id) => validSet.has(id));
167
- }
168
- return validSet.has(m.nodeId);
169
- });
170
- const pruned = before - data.marks.length;
171
- if (pruned > 0)
172
- writeMarkFile(filename, data);
173
- return pruned;
174
- }
175
- /** Rename a mark sidecar file when a document is renamed. */
176
- export function renameMark(oldFilename, newFilename) {
177
- const oldPath = markFilePath(oldFilename);
178
- if (!existsSync(oldPath))
179
- return;
180
- const newPath = markFilePath(newFilename);
181
- renameSync(oldPath, newPath);
182
- }
@@ -1,75 +0,0 @@
1
- /**
2
- * Express routes for GitHub sync.
3
- * Mounted in index.ts — follows version-routes.ts pattern.
4
- */
5
- import { Router } from 'express';
6
- import { getSyncStatus, getCapabilities, getPendingFiles, setupWithGh, setupWithPat, connectExisting, pushSync, } from './git-sync.js';
7
- export function createSyncRouter(broadcastSyncStatus) {
8
- const router = Router();
9
- router.get('/api/sync/status', async (_req, res) => {
10
- try {
11
- res.json(await getSyncStatus());
12
- }
13
- catch (err) {
14
- res.status(500).json({ state: 'error', error: err.message });
15
- }
16
- });
17
- router.get('/api/sync/capabilities', async (_req, res) => {
18
- try {
19
- res.json(await getCapabilities());
20
- }
21
- catch (err) {
22
- res.status(500).json({ error: err.message });
23
- }
24
- });
25
- router.get('/api/sync/pending', async (_req, res) => {
26
- try {
27
- res.json(await getPendingFiles());
28
- }
29
- catch (err) {
30
- res.status(500).json({ error: err.message });
31
- }
32
- });
33
- router.post('/api/sync/setup', async (req, res) => {
34
- try {
35
- const { method, repoName, remoteUrl, pat, isPrivate } = req.body;
36
- if (method === 'gh') {
37
- await setupWithGh(repoName || 'openwriter-docs', isPrivate !== false);
38
- }
39
- else if (method === 'pat') {
40
- if (!pat) {
41
- res.status(400).json({ error: 'PAT is required' });
42
- return;
43
- }
44
- await setupWithPat(pat, repoName || 'openwriter-docs', isPrivate !== false);
45
- }
46
- else if (method === 'connect') {
47
- if (!remoteUrl) {
48
- res.status(400).json({ error: 'Remote URL is required' });
49
- return;
50
- }
51
- await connectExisting(remoteUrl, pat);
52
- }
53
- else {
54
- res.status(400).json({ error: 'Invalid method. Use: gh, pat, or connect' });
55
- return;
56
- }
57
- const status = await getSyncStatus();
58
- broadcastSyncStatus(status);
59
- res.json({ success: true, status });
60
- }
61
- catch (err) {
62
- res.status(500).json({ error: err.message });
63
- }
64
- });
65
- router.post('/api/sync/push', async (_req, res) => {
66
- try {
67
- const result = await pushSync(broadcastSyncStatus);
68
- res.json(result);
69
- }
70
- catch (err) {
71
- res.status(500).json({ state: 'error', error: err.message });
72
- }
73
- });
74
- return router;
75
- }