openwriter 0.29.2 → 0.30.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.
- package/dist/client/assets/index-BtCWWQrZ.css +1 -0
- package/dist/client/assets/{index-DHaZI7nA.js → index-D1naX68L.js} +52 -52
- package/dist/client/index.html +2 -2
- package/dist/plugins/authors-voice/dist/index.d.ts +5 -1
- package/dist/plugins/authors-voice/dist/index.js +24 -1
- package/dist/plugins/github/dist/blog-tools.js +126 -5
- package/dist/plugins/github/dist/helpers.d.ts +2 -0
- package/dist/plugins/github/dist/helpers.js +2 -0
- package/dist/plugins/publish/dist/index.js +6 -1
- package/package.json +3 -2
- package/dist/client/assets/index-Gdw1m46J.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
11
11
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
12
12
|
<link href="https://fonts.googleapis.com/css2?family=Charter:ital,wght@0,400;0,700;1,400&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;0,700;1,400&family=DM+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=DM+Serif+Display&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;500;600;700&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Literata:ital,opsz,wght@0,7..72,400;0,7..72,600;0,7..72,700;1,7..72,400&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,600;1,6..72,400&family=Playfair+Display:wght@400;600;700;900&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;0,8..60,700;1,8..60,400&family=Space+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" />
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-D1naX68L.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BtCWWQrZ.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { Express } from 'express';
|
|
10
10
|
interface PluginConfigField {
|
|
11
|
-
type: 'string' | 'number' | 'boolean';
|
|
11
|
+
type: 'string' | 'number' | 'boolean' | 'select';
|
|
12
12
|
required?: boolean;
|
|
13
13
|
env?: string;
|
|
14
14
|
description?: string;
|
|
15
|
+
options?: Array<{
|
|
16
|
+
value: string;
|
|
17
|
+
label: string;
|
|
18
|
+
}>;
|
|
15
19
|
}
|
|
16
20
|
interface PluginRouteContext {
|
|
17
21
|
app: Express;
|
|
@@ -23,11 +23,27 @@ const plugin = {
|
|
|
23
23
|
env: 'AV_BACKEND_URL',
|
|
24
24
|
description: 'AV backend URL',
|
|
25
25
|
},
|
|
26
|
+
// Model tier for rewrites. Sent as `modelTier` on /api/voice/* (the AV API owns the
|
|
27
|
+
// tier→model map + default — this is a pure pass-through of the user's pick). Blank =
|
|
28
|
+
// API default (strongest). Labels mirror the API's TIER_PICKER_OPTIONS.
|
|
29
|
+
'model': {
|
|
30
|
+
type: 'select',
|
|
31
|
+
env: 'AV_MODEL_TIER',
|
|
32
|
+
description: 'Writing model',
|
|
33
|
+
options: [
|
|
34
|
+
{ value: '', label: 'Default (Strongest)' },
|
|
35
|
+
{ value: 'strongest', label: 'Strongest — Claude Opus (best quality)' },
|
|
36
|
+
{ value: 'gemini-pro', label: 'Gemini Pro — flagship, strong + cheap' },
|
|
37
|
+
{ value: 'balanced', label: 'Balanced — Claude Sonnet' },
|
|
38
|
+
{ value: 'fast', label: 'Fast — Gemini Flash (cheapest)' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
26
41
|
},
|
|
27
42
|
registerRoutes(ctx) {
|
|
28
43
|
const backendUrl = ctx.config['backend-url'] || process.env.AV_BACKEND_URL || 'https://authors-voice.com';
|
|
29
44
|
const apiKey = ctx.config['api-key'] || process.env.AV_API_KEY || '';
|
|
30
45
|
const debugEnabled = process.env.AV_DEBUG === '1' || process.env.AV_DEBUG === 'true';
|
|
46
|
+
const modelTier = ctx.config['model'] || process.env.AV_MODEL_TIER || '';
|
|
31
47
|
const authHeaders = () => {
|
|
32
48
|
const h = { 'Content-Type': 'application/json' };
|
|
33
49
|
if (apiKey)
|
|
@@ -39,6 +55,13 @@ const plugin = {
|
|
|
39
55
|
return body;
|
|
40
56
|
return { ...body, debug: true };
|
|
41
57
|
};
|
|
58
|
+
// Inject the user's model-tier pick. Pure pass-through: the AV API validates the slug
|
|
59
|
+
// and falls back to its own default if absent/unknown. Blank → nothing injected.
|
|
60
|
+
const withModel = (body) => {
|
|
61
|
+
if (!modelTier || !body || typeof body !== 'object')
|
|
62
|
+
return body;
|
|
63
|
+
return { ...body, modelTier };
|
|
64
|
+
};
|
|
42
65
|
// Wildcard proxy for /api/voice/* routes. Pure pass-through: the AV API owns the
|
|
43
66
|
// engine choice (v1/v2) via its own AV_DEFAULT_ENGINE setting, so the plugin injects
|
|
44
67
|
// nothing but the optional owner-only dev debug flag.
|
|
@@ -46,7 +69,7 @@ const plugin = {
|
|
|
46
69
|
try {
|
|
47
70
|
const subPath = req.params[0] || '';
|
|
48
71
|
const targetUrl = `${backendUrl}/api/voice/${subPath}`;
|
|
49
|
-
const body = withDebug(req.body);
|
|
72
|
+
const body = withModel(withDebug(req.body));
|
|
50
73
|
console.log(`[AV Plugin] ${req.method} ${req.path} → ${targetUrl}`);
|
|
51
74
|
const upstream = await fetch(targetUrl, {
|
|
52
75
|
method: 'POST',
|
|
@@ -31,6 +31,33 @@ async function ghAuthOk(cwd) {
|
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Last-resort site-URL detection via the GitHub Pages API. `inferSiteUrl`
|
|
36
|
+
* only reads files committed to the repo (CNAME, wrangler route); this
|
|
37
|
+
* catches GitHub Pages sites whose served URL — custom domain or the
|
|
38
|
+
* `<owner>.github.io/<repo>` default — lives in Pages settings, not a file.
|
|
39
|
+
* Returns the canonical served base URL (trailing slash stripped), or
|
|
40
|
+
* undefined when Pages is not enabled / not accessible. Credential-free
|
|
41
|
+
* (rides the existing `gh auth`); other hosts (Cloudflare Pages, Vercel,
|
|
42
|
+
* Netlify) configure the domain in their dashboard and can't be derived
|
|
43
|
+
* here — those rely on the user supplying site_url.
|
|
44
|
+
*/
|
|
45
|
+
async function inferSiteUrlFromGitHubPages(owner, repo, cwd) {
|
|
46
|
+
try {
|
|
47
|
+
const out = await exec('gh', ['api', `repos/${owner}/${repo}/pages`, '--jq', '.html_url'], cwd);
|
|
48
|
+
const url = out.split('\n')[0].trim();
|
|
49
|
+
if (/^https?:\/\//i.test(url))
|
|
50
|
+
return url.replace(/\/+$/, '');
|
|
51
|
+
}
|
|
52
|
+
catch { /* Pages not enabled, or no access — fall through */ }
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
// Shared hint surfaced when site_url couldn't be determined, so the agent
|
|
56
|
+
// knows to ask the user rather than silently shipping posts with no live link.
|
|
57
|
+
const SITE_URL_HINT = 'Could not auto-detect the public site URL (no CNAME, wrangler route, or GitHub Pages config — ' +
|
|
58
|
+
'common for Cloudflare Pages / Vercel / Netlify sites whose domain lives in the host dashboard). ' +
|
|
59
|
+
'Ask the user for the public base URL (e.g. https://example.com) and set it via site_url. ' +
|
|
60
|
+
'Without it, published posts get no clickable "View Post" link (only the commit/file).';
|
|
34
61
|
function slugify(s) {
|
|
35
62
|
return s.toLowerCase().replace(/['"]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80);
|
|
36
63
|
}
|
|
@@ -535,6 +562,18 @@ export function buildFrontmatter(title, blogCtx, site, coverImagePath) {
|
|
|
535
562
|
if (!fm[dateDest] && !(publishedDateDest && fm[publishedDateDest])) {
|
|
536
563
|
fm[dateDest] = new Date().toISOString().slice(0, 10);
|
|
537
564
|
}
|
|
565
|
+
// Date fields emit as UNQUOTED yaml scalars (pubDate: 2026-05-31), never
|
|
566
|
+
// quoted strings. Astro's z.date() rejects a quoted value — js-yaml parses it
|
|
567
|
+
// as a String, not a Date — which froze a live Netlify build (paybotapp.com,
|
|
568
|
+
// 2026-06-01). The unquoted form is ALSO accepted by z.coerce.date() and by
|
|
569
|
+
// Jekyll/Hugo/Next (gray-matter), so it is the universally-correct emit.
|
|
570
|
+
// adr: adr/blog-image-contract.md
|
|
571
|
+
const dateKeys = new Set([dateDest]);
|
|
572
|
+
if (publishedDateDest)
|
|
573
|
+
dateKeys.add(publishedDateDest);
|
|
574
|
+
const emitLine = (k, v) => dateKeys.has(k) && typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v)
|
|
575
|
+
? `${k}: ${v}`
|
|
576
|
+
: `${k}: ${yamlValue(v)}`;
|
|
538
577
|
// Emit in stable order: defaults first (in their declared order),
|
|
539
578
|
// then title, then any new keys we added
|
|
540
579
|
const lines = [];
|
|
@@ -542,7 +581,7 @@ export function buildFrontmatter(title, blogCtx, site, coverImagePath) {
|
|
|
542
581
|
if (site.frontmatter_defaults) {
|
|
543
582
|
for (const k of Object.keys(site.frontmatter_defaults)) {
|
|
544
583
|
if (k in fm) {
|
|
545
|
-
lines.push(
|
|
584
|
+
lines.push(emitLine(k, fm[k]));
|
|
546
585
|
written.add(k);
|
|
547
586
|
}
|
|
548
587
|
}
|
|
@@ -554,7 +593,7 @@ export function buildFrontmatter(title, blogCtx, site, coverImagePath) {
|
|
|
554
593
|
for (const [k, v] of Object.entries(fm)) {
|
|
555
594
|
if (written.has(k))
|
|
556
595
|
continue;
|
|
557
|
-
lines.push(
|
|
596
|
+
lines.push(emitLine(k, v));
|
|
558
597
|
written.add(k);
|
|
559
598
|
}
|
|
560
599
|
return `---\n${lines.join('\n')}\n---\n\n`;
|
|
@@ -653,7 +692,10 @@ export function blogTools() {
|
|
|
653
692
|
const confidence = framework !== 'unknown' && detected ? 'high'
|
|
654
693
|
: detected ? 'medium'
|
|
655
694
|
: 'low';
|
|
656
|
-
|
|
695
|
+
// site_url: prefer files committed to the repo (CNAME / wrangler route),
|
|
696
|
+
// then fall back to the GitHub Pages API for GH-hosted sites whose served
|
|
697
|
+
// URL lives in Pages settings rather than a committed file.
|
|
698
|
+
const siteUrl = inferSiteUrl(cloneDir) || await inferSiteUrlFromGitHubPages(owner, repo, cacheRoot);
|
|
657
699
|
return {
|
|
658
700
|
owner,
|
|
659
701
|
repo,
|
|
@@ -669,6 +711,9 @@ export function blogTools() {
|
|
|
669
711
|
// Always propose a pattern even when site_url is unknown so the user can fill in the URL
|
|
670
712
|
site_url: siteUrl,
|
|
671
713
|
blog_url_pattern: '/blog/{slug}/',
|
|
714
|
+
// When site_url couldn't be derived, tell the agent to ask the user —
|
|
715
|
+
// otherwise it ships posts with no live "View Post" link, silently.
|
|
716
|
+
...(siteUrl ? {} : { needs_site_url: true, site_url_hint: SITE_URL_HINT }),
|
|
672
717
|
samples_analyzed: samplesAfterFilter.length,
|
|
673
718
|
samples_skipped_openwriter_leak: samplesSkipped,
|
|
674
719
|
markdown_files_found: mdBest?.count ?? 0,
|
|
@@ -707,7 +752,7 @@ export function blogTools() {
|
|
|
707
752
|
},
|
|
708
753
|
site_url: {
|
|
709
754
|
type: 'string',
|
|
710
|
-
description: 'Public base URL of the site (e.g. "https://example.com"). Used to construct the live URL surfaced after publish. inspect_blog_repo proposes this from CNAME / wrangler.toml when found.',
|
|
755
|
+
description: 'Public base URL of the site (e.g. "https://example.com"). Used to construct the live "View Post" URL surfaced after publish. inspect_blog_repo proposes this from CNAME / wrangler.toml / the GitHub Pages API when found; for Cloudflare Pages / Vercel / Netlify (domain configured in the host dashboard) it can\'t be auto-detected — ask the user and pass it here. If omitted, the response returns needs_site_url so you know to follow up; backfill later with edit_blog_site.',
|
|
711
756
|
},
|
|
712
757
|
blog_url_pattern: {
|
|
713
758
|
type: 'string',
|
|
@@ -752,7 +797,12 @@ export function blogTools() {
|
|
|
752
797
|
const sites = await listBlogSites();
|
|
753
798
|
sites.push(site);
|
|
754
799
|
await writeBlogSites(sites);
|
|
755
|
-
return {
|
|
800
|
+
return {
|
|
801
|
+
success: true,
|
|
802
|
+
site,
|
|
803
|
+
// No site_url ⇒ no live link on publish. Tell the agent to follow up.
|
|
804
|
+
...(site.site_url ? {} : { needs_site_url: true, site_url_hint: SITE_URL_HINT }),
|
|
805
|
+
};
|
|
756
806
|
},
|
|
757
807
|
},
|
|
758
808
|
{
|
|
@@ -784,6 +834,68 @@ export function blogTools() {
|
|
|
784
834
|
return { success: true, removed: id };
|
|
785
835
|
},
|
|
786
836
|
},
|
|
837
|
+
{
|
|
838
|
+
name: 'edit_blog_site',
|
|
839
|
+
description: 'Update fields on an already-registered blog site by id. Only the fields you pass change; everything else is left intact. The common use is backfilling site_url / blog_url_pattern after registration so published posts get a live "View Post" link (e.g. for Cloudflare Pages / Vercel / Netlify sites where the domain couldn\'t be auto-detected).',
|
|
840
|
+
inputSchema: {
|
|
841
|
+
type: 'object',
|
|
842
|
+
properties: {
|
|
843
|
+
id: { type: 'string', description: 'Blog site id (from list_blog_sites)' },
|
|
844
|
+
label: { type: 'string', description: 'User-facing name' },
|
|
845
|
+
branch: { type: 'string', description: 'Branch to push to' },
|
|
846
|
+
content_dir: { type: 'string', description: 'Directory where post .md files live' },
|
|
847
|
+
image_dir: { type: 'string', description: 'Directory where image files write' },
|
|
848
|
+
image_public_prefix: { type: 'string', description: 'Directory prefix images live under' },
|
|
849
|
+
image_path_style: { type: 'string', enum: ['relative', 'absolute'], description: 'How image paths are written' },
|
|
850
|
+
image_naming: { type: 'string', description: 'Cover filename template with `{slug}` + `{ext}`' },
|
|
851
|
+
framework: { type: 'string', enum: ['astro', 'next', 'jekyll', 'hugo', 'unknown'], description: 'Site framework' },
|
|
852
|
+
frontmatter_defaults: { type: 'object', description: 'Constants applied to every post\'s frontmatter' },
|
|
853
|
+
frontmatter_field_map: { type: 'object', description: 'Rename map: openwriter blogContext key → site frontmatter key' },
|
|
854
|
+
frontmatter_schema: { type: 'array', items: { type: 'string' }, description: 'List of frontmatter keys the site uses' },
|
|
855
|
+
site_url: { type: 'string', description: 'Public base URL (e.g. "https://example.com"). Pass an empty string to clear it.' },
|
|
856
|
+
blog_url_pattern: { type: 'string', description: 'URL path pattern with `{slug}` placeholder (e.g. "/blog/{slug}/").' },
|
|
857
|
+
},
|
|
858
|
+
required: ['id'],
|
|
859
|
+
},
|
|
860
|
+
handler: async (params) => {
|
|
861
|
+
const id = String(params.id);
|
|
862
|
+
const sites = await listBlogSites();
|
|
863
|
+
const site = sites.find(s => s.id === id);
|
|
864
|
+
if (!site)
|
|
865
|
+
return { error: `No blog site with id ${id}` };
|
|
866
|
+
// Plain string fields — set when a non-empty string is provided.
|
|
867
|
+
for (const key of ['label', 'branch', 'content_dir', 'image_dir', 'image_public_prefix', 'image_naming', 'blog_url_pattern']) {
|
|
868
|
+
if (typeof params[key] === 'string' && params[key].trim()) {
|
|
869
|
+
site[key] = params[key].trim();
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
// site_url: trim + strip trailing slash; empty string clears it.
|
|
873
|
+
if (typeof params.site_url === 'string') {
|
|
874
|
+
const v = params.site_url.trim().replace(/\/+$/, '');
|
|
875
|
+
if (v)
|
|
876
|
+
site.site_url = v;
|
|
877
|
+
else
|
|
878
|
+
delete site.site_url;
|
|
879
|
+
}
|
|
880
|
+
if (params.image_path_style === 'relative' || params.image_path_style === 'absolute') {
|
|
881
|
+
site.image_path_style = params.image_path_style;
|
|
882
|
+
}
|
|
883
|
+
if (params.framework && ['astro', 'next', 'jekyll', 'hugo', 'unknown'].includes(String(params.framework))) {
|
|
884
|
+
site.framework = params.framework;
|
|
885
|
+
}
|
|
886
|
+
if (params.frontmatter_defaults && typeof params.frontmatter_defaults === 'object') {
|
|
887
|
+
site.frontmatter_defaults = params.frontmatter_defaults;
|
|
888
|
+
}
|
|
889
|
+
if (params.frontmatter_field_map && typeof params.frontmatter_field_map === 'object') {
|
|
890
|
+
site.frontmatter_field_map = params.frontmatter_field_map;
|
|
891
|
+
}
|
|
892
|
+
if (Array.isArray(params.frontmatter_schema)) {
|
|
893
|
+
site.frontmatter_schema = params.frontmatter_schema.map(String);
|
|
894
|
+
}
|
|
895
|
+
await writeBlogSites(sites);
|
|
896
|
+
return { success: true, site };
|
|
897
|
+
},
|
|
898
|
+
},
|
|
787
899
|
{
|
|
788
900
|
name: 'post_to_blog',
|
|
789
901
|
description: 'Publish the active OpenWriter document to a registered GitHub blog site via local git ops (clone-or-pull, write file + images, commit, push). Auth uses your existing `gh auth login`.',
|
|
@@ -979,6 +1091,15 @@ export function blogTools() {
|
|
|
979
1091
|
// Same convention mcp.ts:1112 uses for active-doc metadata writes.
|
|
980
1092
|
srv.bumpDocVersion();
|
|
981
1093
|
srv.save();
|
|
1094
|
+
// Notify every connected client so the file-tree "published" ✓ + the
|
|
1095
|
+
// "Republish to Blog" context-menu label + the compose-view "Published"
|
|
1096
|
+
// pill flip live, with no manual reload. metadata-changed updates the
|
|
1097
|
+
// active doc's compose view; documents-changed re-reads /api/documents
|
|
1098
|
+
// (where blogContext.lastPublish.publishedUrl → postedUrl drives the
|
|
1099
|
+
// file tree). Mirrors the broadcast-after-setMetadata convention the
|
|
1100
|
+
// core MCP tools follow. adr: adr/plugin-metadata-broadcast.md
|
|
1101
|
+
srv.broadcastMetadataChanged(srv.getMetadata());
|
|
1102
|
+
srv.broadcastDocumentsChanged();
|
|
982
1103
|
}
|
|
983
1104
|
catch (err) {
|
|
984
1105
|
writebackWarning = `Published successfully, but failed to mark doc as sent: ${err.message}`;
|
|
@@ -15,6 +15,8 @@ export interface ServerModules {
|
|
|
15
15
|
setMetadata: (updates: Record<string, any>) => void;
|
|
16
16
|
bumpDocVersion: () => number;
|
|
17
17
|
broadcastSyncStatus: (status: any) => void;
|
|
18
|
+
broadcastMetadataChanged: (metadata: Record<string, any>) => void;
|
|
19
|
+
broadcastDocumentsChanged: () => void;
|
|
18
20
|
tiptapToMarkdown: (doc: any, title: string, metadata?: Record<string, any>) => string;
|
|
19
21
|
}
|
|
20
22
|
export declare function getServerModules(): Promise<ServerModules>;
|
|
@@ -39,6 +39,8 @@ export async function getServerModules() {
|
|
|
39
39
|
setMetadata: state.setMetadata,
|
|
40
40
|
bumpDocVersion: state.bumpDocVersion,
|
|
41
41
|
broadcastSyncStatus: ws.broadcastSyncStatus,
|
|
42
|
+
broadcastMetadataChanged: ws.broadcastMetadataChanged,
|
|
43
|
+
broadcastDocumentsChanged: ws.broadcastDocumentsChanged,
|
|
42
44
|
tiptapToMarkdown: markdown.tiptapToMarkdown,
|
|
43
45
|
};
|
|
44
46
|
return _cached;
|
|
@@ -300,6 +300,7 @@ const plugin = {
|
|
|
300
300
|
properties: {
|
|
301
301
|
content: { type: 'string', description: 'Tweet text (max 280 characters)' },
|
|
302
302
|
connection_id: { type: 'string', description: 'X connection ID. If omitted, uses the first active X connection.' },
|
|
303
|
+
autoplug: { type: 'boolean', description: 'Eligible for engagement autoplugs (default true). Pass false to opt this post out of autoplug tracking.' },
|
|
303
304
|
},
|
|
304
305
|
required: ['content'],
|
|
305
306
|
},
|
|
@@ -321,7 +322,7 @@ const plugin = {
|
|
|
321
322
|
const server = await getServerModules();
|
|
322
323
|
const res = await server.platformFetch(`/connections/${id}/post`, {
|
|
323
324
|
method: 'POST',
|
|
324
|
-
body: JSON.stringify({ content: params.content }),
|
|
325
|
+
body: JSON.stringify({ content: params.content, no_autoplug: params.autoplug === false }),
|
|
325
326
|
});
|
|
326
327
|
const data = await res.json();
|
|
327
328
|
if (!res.ok)
|
|
@@ -500,6 +501,10 @@ const plugin = {
|
|
|
500
501
|
totalMedia = mediaIds.length;
|
|
501
502
|
queueContent = mediaIds.length > 0 ? { text, mediaIds } : { text };
|
|
502
503
|
}
|
|
504
|
+
// Honor the doc's autoplug opt-out — rides inside the content JSONB so
|
|
505
|
+
// the platform cron skips enrollment for this scheduled post.
|
|
506
|
+
if (metadata?.autoplug === false)
|
|
507
|
+
queueContent.no_autoplug = true;
|
|
503
508
|
// --- Queue it ---
|
|
504
509
|
const body = {
|
|
505
510
|
content: queueContent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openwriter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.1",
|
|
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",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"./plugin-types": "./dist/server/plugin-types.js"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"build": "vite build && tsc -p tsconfig.server.json",
|
|
34
|
+
"build": "vite build && tsc -p tsconfig.server.json && npm run build:plugins",
|
|
35
|
+
"build:plugins": "node scripts/build-plugins.cjs",
|
|
35
36
|
"prepublishOnly": "node scripts/prepublish.cjs",
|
|
36
37
|
"preview": "node dist/bin/pad.js",
|
|
37
38
|
"lint": "eslint src server bin --ext .ts,.tsx"
|