openwriter 0.5.4 → 0.6.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/bin/pad.js +3 -0
- package/dist/client/assets/index-CCMCrgTu.js +209 -0
- package/dist/client/assets/index-DN1_4Au6.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/connection-routes.js +327 -0
- package/dist/server/connections.js +35 -0
- package/dist/server/documents.js +30 -24
- package/dist/server/export-html-template.js +1 -1
- package/dist/server/export-routes.js +1 -1
- package/dist/server/gdoc-import.js +2 -2
- package/dist/server/git-sync.js +30 -30
- package/dist/server/helpers.js +175 -20
- package/dist/server/image-upload.js +10 -7
- package/dist/server/index.js +162 -5
- package/dist/server/marks.js +11 -11
- package/dist/server/mcp.js +134 -50
- package/dist/server/plugin-manager.js +2 -2
- package/dist/server/scheduler-routes.js +121 -0
- package/dist/server/state.js +126 -42
- package/dist/server/versions.js +6 -2
- package/dist/server/workspace-routes.js +14 -13
- package/dist/server/workspaces.js +7 -7
- package/dist/server/ws.js +15 -0
- package/package.json +1 -1
- package/skill/SKILL.md +95 -11
- package/dist/client/assets/index-BAbqg4Q8.js +0 -210
- package/dist/client/assets/index-BR_sMmFf.css +0 -1
package/dist/server/mcp.js
CHANGED
|
@@ -9,12 +9,13 @@ import { randomUUID } from 'crypto';
|
|
|
9
9
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import {
|
|
12
|
+
import { getDataDir, ensureDataDir, resolveDocPath } from './helpers.js';
|
|
13
13
|
import { getDocument, getWordCount, getPendingChangeCount, getTitle, getStatus, getNodesByIds, getMetadata, setMetadata, applyChanges, applyTextEdits, updateDocument, save, markAllNodesAsPending, setAgentLock, updatePendingCacheForActiveDoc, populateDocumentFile, applyChangesToFile, applyTextEditsToFile, getDocId, getFilePath, } from './state.js';
|
|
14
14
|
import { listDocuments, switchDocument, createDocument, createDocumentFile, deleteDocument, openFile, getActiveFilename, updateDocumentTitle, promoteTempFile, archiveDocument, unarchiveDocument, resolveDocId } from './documents.js';
|
|
15
15
|
import { broadcastDocumentSwitched, broadcastDocumentsChanged, broadcastWorkspacesChanged, broadcastTitleChanged, broadcastMetadataChanged, broadcastPendingDocsChanged, broadcastWritingStarted, broadcastWritingFinished } from './ws.js';
|
|
16
|
-
import { listWorkspaces, getWorkspace, getDocTitle, getItemContext, addDoc, updateWorkspaceContext, createWorkspace, deleteWorkspace, addContainerToWorkspace, findOrCreateWorkspace, findOrCreateContainer, moveDoc, renameWorkspace, renameContainer } from './workspaces.js';
|
|
16
|
+
import { listWorkspaces, getWorkspace, getDocTitle, getItemContext, addDoc, updateWorkspaceContext, createWorkspace, deleteWorkspace, addContainerToWorkspace, findOrCreateWorkspace, findOrCreateContainer, moveDoc, removeContainer, renameWorkspace, renameContainer } from './workspaces.js';
|
|
17
17
|
import { addDocTag, removeDocTag, getDocTagsByFilename, getCachedDocument } from './state.js';
|
|
18
|
+
import { findDocNode } from './workspace-tree.js';
|
|
18
19
|
import { importGoogleDoc } from './gdoc-import.js';
|
|
19
20
|
import { toCompactFormat, compactNodes, parseMarkdownContent, mergeParagraphsToHardBreaks } from './compact.js';
|
|
20
21
|
import matter from 'gray-matter';
|
|
@@ -23,6 +24,19 @@ import { listVersions, forceSnapshot, restoreVersion } from './versions.js';
|
|
|
23
24
|
import { markdownToTiptap } from './markdown.js';
|
|
24
25
|
import { getMarks, getMarkCount, getGlobalMarkSummary, resolveMarks } from './marks.js';
|
|
25
26
|
import { broadcastMarksChanged } from './ws.js';
|
|
27
|
+
/** Map a content type string to its frontmatter metadata object. */
|
|
28
|
+
function resolveTypeMeta(type) {
|
|
29
|
+
switch (type) {
|
|
30
|
+
case 'tweet': return { tweetContext: { mode: 'tweet' } };
|
|
31
|
+
case 'reply': return { tweetContext: { mode: 'reply' } };
|
|
32
|
+
case 'quote': return { tweetContext: { mode: 'quote' } };
|
|
33
|
+
case 'article': return { articleContext: { active: true } };
|
|
34
|
+
case 'linkedin': return { linkedinContext: { active: true } };
|
|
35
|
+
case 'newsletter': return { newsletterContext: { active: true } };
|
|
36
|
+
case 'blog': return { blogContext: { active: true } };
|
|
37
|
+
default: return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
26
40
|
/** Check if a document is in tweet compose mode (has tweetContext metadata). */
|
|
27
41
|
function isTweetDoc(filename) {
|
|
28
42
|
if (!filename || filename === getActiveFilename()) {
|
|
@@ -171,15 +185,24 @@ export const TOOL_REGISTRY = [
|
|
|
171
185
|
},
|
|
172
186
|
{
|
|
173
187
|
name: 'create_document',
|
|
174
|
-
description: 'Create a new empty document and switch to it. Always provide a title. Saves the current document first. By default shows a sidebar spinner that persists until populate_document is called — set empty=true to skip the spinner and switch immediately (use for template docs like tweets/articles that don\'t need agent content). If workspace is provided, the doc is automatically added to it (workspace is created if it doesn\'t exist). If container is also provided, the doc is placed inside that container (created if it doesn\'t exist).',
|
|
188
|
+
description: 'Create a new empty document and switch to it. Always provide a title. Saves the current document first. By default shows a sidebar spinner that persists until populate_document is called — set empty=true to skip the spinner and switch immediately (use for template docs like tweets/articles that don\'t need agent content). If workspace is provided, the doc is automatically added to it (workspace is created if it doesn\'t exist). If container is also provided, the doc is placed inside that container (created if it doesn\'t exist). Use content_type to create typed documents (tweet, article, linkedin, etc.) with the correct metadata pre-set.',
|
|
175
189
|
schema: {
|
|
176
190
|
title: z.string().optional().describe('Title for the new document. Defaults to "Untitled".'),
|
|
177
191
|
path: z.string().optional().describe('Absolute file path to create the document at (e.g. "C:/projects/doc.md"). If omitted, creates in ~/.openwriter/.'),
|
|
178
192
|
workspace: z.string().optional().describe('Workspace title to add this doc to. Creates the workspace if it doesn\'t exist.'),
|
|
179
193
|
container: z.string().optional().describe('Container name within the workspace (e.g. "Chapters", "Notes", "References"). Creates the container if it doesn\'t exist. Requires workspace.'),
|
|
180
194
|
empty: z.boolean().optional().describe('If true, skip the writing spinner and switch to the doc immediately. No need to call populate_document. Use for template docs (tweets, articles) that start empty.'),
|
|
181
|
-
|
|
182
|
-
|
|
195
|
+
content_type: z.string().optional().describe('Content type: tweet, reply, quote, article, linkedin, newsletter, or blog. Sets metadata so the doc is recognized as that type. For reply/quote, use set_metadata after creation to set the target tweet URL.'),
|
|
196
|
+
},
|
|
197
|
+
handler: async ({ title, path, workspace, container, empty, content_type }) => {
|
|
198
|
+
// Default title from content_type if not provided
|
|
199
|
+
if (!title && content_type) {
|
|
200
|
+
const typeDefaults = {
|
|
201
|
+
tweet: 'Tweet', reply: 'Reply', quote: 'Quote Tweet', article: 'Article',
|
|
202
|
+
linkedin: 'LinkedIn Post', newsletter: 'Newsletter', blog: 'Blog Post',
|
|
203
|
+
};
|
|
204
|
+
title = typeDefaults[content_type];
|
|
205
|
+
}
|
|
183
206
|
// Resolve workspace/container up front so spinner renders in the right place
|
|
184
207
|
let wsTarget;
|
|
185
208
|
if (workspace) {
|
|
@@ -202,6 +225,13 @@ export const TOOL_REGISTRY = [
|
|
|
202
225
|
// Immediate switch — no spinner, no populate_document needed
|
|
203
226
|
setAgentLock();
|
|
204
227
|
const result = createDocument(title, undefined, path);
|
|
228
|
+
// Apply type-specific metadata
|
|
229
|
+
if (content_type) {
|
|
230
|
+
const typeMeta = resolveTypeMeta(content_type);
|
|
231
|
+
if (typeMeta) {
|
|
232
|
+
setMetadata(typeMeta);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
205
235
|
let wsInfo = '';
|
|
206
236
|
if (wsTarget) {
|
|
207
237
|
addDoc(wsTarget.wsFilename, wsTarget.containerId, result.filename, result.title);
|
|
@@ -211,17 +241,18 @@ export const TOOL_REGISTRY = [
|
|
|
211
241
|
save();
|
|
212
242
|
broadcastDocumentsChanged();
|
|
213
243
|
broadcastWorkspacesChanged();
|
|
214
|
-
broadcastDocumentSwitched(getDocument(), getTitle(), getActiveFilename());
|
|
244
|
+
broadcastDocumentSwitched(getDocument(), getTitle(), getActiveFilename(), getMetadata());
|
|
215
245
|
return {
|
|
216
246
|
content: [{
|
|
217
247
|
type: 'text',
|
|
218
|
-
text: `Created "${result.title}" [${newDocId}]${wsInfo} — ready.`,
|
|
248
|
+
text: `Created "${result.title}" [${newDocId}]${wsInfo}${content_type ? ` (${content_type})` : ''} — ready.`,
|
|
219
249
|
}],
|
|
220
250
|
};
|
|
221
251
|
}
|
|
222
252
|
// Two-step flow: create file on disk WITHOUT switching the user's view.
|
|
223
253
|
// The spinner persists in the sidebar until populate_document is called.
|
|
224
|
-
const
|
|
254
|
+
const typeMeta = content_type ? resolveTypeMeta(content_type) : undefined;
|
|
255
|
+
const result = createDocumentFile(title, path, typeMeta);
|
|
225
256
|
let wsInfo = '';
|
|
226
257
|
if (wsTarget) {
|
|
227
258
|
addDoc(wsTarget.wsFilename, wsTarget.containerId, result.filename, result.title);
|
|
@@ -507,27 +538,11 @@ export const TOOL_REGISTRY = [
|
|
|
507
538
|
description: 'Get progressive disclosure context for a document in a workspace: workspace-level context (characters, settings, rules) and tags. Use before writing to understand context.',
|
|
508
539
|
schema: {
|
|
509
540
|
workspaceFile: z.string().describe('Workspace manifest filename'),
|
|
510
|
-
|
|
511
|
-
},
|
|
512
|
-
handler: async ({ workspaceFile, docFile }) => {
|
|
513
|
-
return { content: [{ type: 'text', text: JSON.stringify(getItemContext(workspaceFile, docFile), null, 2) }] };
|
|
541
|
+
docId: z.string().describe('Document docId (8-char hex from list_documents)'),
|
|
514
542
|
},
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
description: 'Add a document to a workspace. Optionally place it inside a container.',
|
|
519
|
-
schema: {
|
|
520
|
-
workspaceFile: z.string().describe('Workspace manifest filename'),
|
|
521
|
-
docFile: z.string().describe('Document filename to add (e.g. "Chapter 1.md")'),
|
|
522
|
-
containerId: z.string().optional().describe('Container ID to add into (null = root level)'),
|
|
523
|
-
title: z.string().optional().describe('Display title for the doc'),
|
|
524
|
-
},
|
|
525
|
-
handler: async ({ workspaceFile, docFile, containerId, title }) => {
|
|
526
|
-
addDoc(workspaceFile, containerId ?? null, docFile, title || docFile.replace(/\.md$/, ''));
|
|
527
|
-
broadcastWorkspacesChanged();
|
|
528
|
-
return {
|
|
529
|
-
content: [{ type: 'text', text: `Added "${docFile}" to workspace${containerId ? ` in container ${containerId}` : ''}` }],
|
|
530
|
-
};
|
|
543
|
+
handler: async ({ workspaceFile, docId }) => {
|
|
544
|
+
const filename = resolveDocId(docId);
|
|
545
|
+
return { content: [{ type: 'text', text: JSON.stringify(getItemContext(workspaceFile, filename), null, 2) }] };
|
|
531
546
|
},
|
|
532
547
|
},
|
|
533
548
|
{
|
|
@@ -561,45 +576,70 @@ export const TOOL_REGISTRY = [
|
|
|
561
576
|
return { content: [{ type: 'text', text: `Created container "${name}" (id:${result.containerId})` }] };
|
|
562
577
|
},
|
|
563
578
|
},
|
|
579
|
+
{
|
|
580
|
+
name: 'delete_container',
|
|
581
|
+
description: 'Delete a container from a workspace. Any docs inside are removed from the workspace (files are NOT deleted from disk).',
|
|
582
|
+
schema: {
|
|
583
|
+
workspaceFile: z.string().describe('Workspace manifest filename'),
|
|
584
|
+
containerId: z.string().describe('Container ID to delete'),
|
|
585
|
+
},
|
|
586
|
+
handler: async ({ workspaceFile, containerId }) => {
|
|
587
|
+
removeContainer(workspaceFile, containerId);
|
|
588
|
+
broadcastWorkspacesChanged();
|
|
589
|
+
return { content: [{ type: 'text', text: `Deleted container ${containerId}` }] };
|
|
590
|
+
},
|
|
591
|
+
},
|
|
564
592
|
{
|
|
565
593
|
name: 'tag_doc',
|
|
566
594
|
description: 'Add a tag to a document. Tags are stored in the document\'s frontmatter — they travel with the file. A doc can have multiple tags.',
|
|
567
595
|
schema: {
|
|
568
|
-
|
|
596
|
+
docId: z.string().describe('Document docId (8-char hex from list_documents)'),
|
|
569
597
|
tag: z.string().describe('Tag name to add'),
|
|
570
598
|
},
|
|
571
|
-
handler: async ({
|
|
572
|
-
|
|
599
|
+
handler: async ({ docId, tag }) => {
|
|
600
|
+
const filename = resolveDocId(docId);
|
|
601
|
+
addDocTag(filename, tag);
|
|
573
602
|
broadcastDocumentsChanged();
|
|
574
|
-
return { content: [{ type: 'text', text: `Tagged "${
|
|
603
|
+
return { content: [{ type: 'text', text: `Tagged "${filename}" with [${tag}]` }] };
|
|
575
604
|
},
|
|
576
605
|
},
|
|
577
606
|
{
|
|
578
607
|
name: 'untag_doc',
|
|
579
608
|
description: 'Remove a tag from a document.',
|
|
580
609
|
schema: {
|
|
581
|
-
|
|
610
|
+
docId: z.string().describe('Document docId (8-char hex from list_documents)'),
|
|
582
611
|
tag: z.string().describe('Tag name to remove'),
|
|
583
612
|
},
|
|
584
|
-
handler: async ({
|
|
585
|
-
|
|
613
|
+
handler: async ({ docId, tag }) => {
|
|
614
|
+
const filename = resolveDocId(docId);
|
|
615
|
+
removeDocTag(filename, tag);
|
|
586
616
|
broadcastDocumentsChanged();
|
|
587
|
-
return { content: [{ type: 'text', text: `Removed tag [${tag}] from "${
|
|
617
|
+
return { content: [{ type: 'text', text: `Removed tag [${tag}] from "${filename}"` }] };
|
|
588
618
|
},
|
|
589
619
|
},
|
|
590
620
|
{
|
|
591
621
|
name: 'move_doc',
|
|
592
|
-
description: '
|
|
622
|
+
description: 'Add a document to a workspace, or move it within the workspace. If the doc is not yet in the workspace it will be added; if it is already present it will be moved to the target container.',
|
|
593
623
|
schema: {
|
|
594
624
|
workspaceFile: z.string().describe('Workspace manifest filename'),
|
|
595
|
-
|
|
625
|
+
docId: z.string().describe('Document docId (8-char hex from list_documents)'),
|
|
596
626
|
targetContainerId: z.string().optional().describe('Target container ID (omit for root level)'),
|
|
597
627
|
afterFile: z.string().optional().describe('Place after this file (omit for beginning)'),
|
|
598
628
|
},
|
|
599
|
-
handler: async ({ workspaceFile,
|
|
600
|
-
|
|
629
|
+
handler: async ({ workspaceFile, docId, targetContainerId, afterFile }) => {
|
|
630
|
+
const filename = resolveDocId(docId);
|
|
631
|
+
const ws = getWorkspace(workspaceFile);
|
|
632
|
+
const existing = findDocNode(ws.root, filename);
|
|
633
|
+
if (existing) {
|
|
634
|
+
moveDoc(workspaceFile, filename, targetContainerId ?? null, afterFile ?? null);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
const title = getDocTitle(filename);
|
|
638
|
+
addDoc(workspaceFile, targetContainerId ?? null, filename, title, afterFile ?? null);
|
|
639
|
+
}
|
|
601
640
|
broadcastWorkspacesChanged();
|
|
602
|
-
|
|
641
|
+
const action = existing ? 'Moved' : 'Added';
|
|
642
|
+
return { content: [{ type: 'text', text: `${action} "${filename}"${targetContainerId ? ` to container ${targetContainerId}` : ' to root'}` }] };
|
|
603
643
|
},
|
|
604
644
|
},
|
|
605
645
|
{
|
|
@@ -726,7 +766,7 @@ export const TOOL_REGISTRY = [
|
|
|
726
766
|
}
|
|
727
767
|
// Save to ~/.openwriter/_images/
|
|
728
768
|
ensureDataDir();
|
|
729
|
-
const imagesDir = join(
|
|
769
|
+
const imagesDir = join(getDataDir(), '_images');
|
|
730
770
|
if (!existsSync(imagesDir))
|
|
731
771
|
mkdirSync(imagesDir, { recursive: true });
|
|
732
772
|
const filename = `${randomUUID().slice(0, 8)}.png`;
|
|
@@ -746,8 +786,10 @@ export const TOOL_REGISTRY = [
|
|
|
746
786
|
}],
|
|
747
787
|
};
|
|
748
788
|
}
|
|
749
|
-
// Use
|
|
750
|
-
|
|
789
|
+
// Use LIVE metadata for coverImages (not stale pre-await snapshot)
|
|
790
|
+
// so concurrent generate_image calls don't overwrite each other's results
|
|
791
|
+
const liveMeta = getMetadata();
|
|
792
|
+
const articleContext = liveMeta.articleContext || {};
|
|
751
793
|
let existing = Array.isArray(articleContext.coverImages) ? [...articleContext.coverImages] : [];
|
|
752
794
|
// Seed with current coverImage if array is empty (first carousel entry)
|
|
753
795
|
if (existing.length === 0 && articleContext.coverImage) {
|
|
@@ -801,7 +843,7 @@ export const TOOL_REGISTRY = [
|
|
|
801
843
|
}
|
|
802
844
|
// Save to ~/.openwriter/_images/
|
|
803
845
|
ensureDataDir();
|
|
804
|
-
const imagesDir = join(
|
|
846
|
+
const imagesDir = join(getDataDir(), '_images');
|
|
805
847
|
if (!existsSync(imagesDir))
|
|
806
848
|
mkdirSync(imagesDir, { recursive: true });
|
|
807
849
|
const imgFilename = `${randomUUID().slice(0, 8)}.png`;
|
|
@@ -950,21 +992,59 @@ export const TOOL_REGISTRY = [
|
|
|
950
992
|
},
|
|
951
993
|
},
|
|
952
994
|
];
|
|
953
|
-
/**
|
|
995
|
+
/** Live MCP server instance — used to register plugin tools dynamically. */
|
|
996
|
+
let mcpServerInstance = null;
|
|
997
|
+
/** Convert a JSON Schema properties object to a Zod shape for MCP tool registration. */
|
|
998
|
+
function jsonSchemaToZodShape(inputSchema) {
|
|
999
|
+
const properties = (inputSchema.properties || {});
|
|
1000
|
+
const required = new Set((inputSchema.required || []));
|
|
1001
|
+
const shape = {};
|
|
1002
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
1003
|
+
let field;
|
|
1004
|
+
switch (prop.type) {
|
|
1005
|
+
case 'number':
|
|
1006
|
+
field = z.number();
|
|
1007
|
+
break;
|
|
1008
|
+
case 'boolean':
|
|
1009
|
+
field = z.boolean();
|
|
1010
|
+
break;
|
|
1011
|
+
default:
|
|
1012
|
+
field = z.string();
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
if (prop.description)
|
|
1016
|
+
field = field.describe(prop.description);
|
|
1017
|
+
if (!required.has(key))
|
|
1018
|
+
field = field.optional();
|
|
1019
|
+
shape[key] = field;
|
|
1020
|
+
}
|
|
1021
|
+
return shape;
|
|
1022
|
+
}
|
|
1023
|
+
/** Register MCP tools from plugins. Dynamically adds to the live MCP session. */
|
|
954
1024
|
export function registerPluginTools(tools) {
|
|
955
1025
|
for (const tool of tools) {
|
|
956
|
-
|
|
1026
|
+
const zodShape = jsonSchemaToZodShape(tool.inputSchema);
|
|
1027
|
+
const toolDef = {
|
|
957
1028
|
name: tool.name,
|
|
958
1029
|
description: tool.description,
|
|
959
|
-
schema:
|
|
1030
|
+
schema: zodShape,
|
|
960
1031
|
handler: async (args) => {
|
|
961
1032
|
const result = await tool.handler(args);
|
|
962
1033
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
963
1034
|
},
|
|
964
|
-
}
|
|
1035
|
+
};
|
|
1036
|
+
TOOL_REGISTRY.push(toolDef);
|
|
1037
|
+
// Register on live MCP server so existing sessions see it immediately
|
|
1038
|
+
if (mcpServerInstance) {
|
|
1039
|
+
mcpServerInstance.tool(tool.name, tool.description, zodShape, toolDef.handler);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// Notify connected clients that the tool list changed
|
|
1043
|
+
if (mcpServerInstance) {
|
|
1044
|
+
mcpServerInstance.server.sendToolListChanged().catch(() => { });
|
|
965
1045
|
}
|
|
966
1046
|
}
|
|
967
|
-
/** Remove MCP tools by name.
|
|
1047
|
+
/** Remove MCP tools by name. Notifies connected clients of the change. */
|
|
968
1048
|
export function removePluginTools(names) {
|
|
969
1049
|
const nameSet = new Set(names);
|
|
970
1050
|
for (let i = TOOL_REGISTRY.length - 1; i >= 0; i--) {
|
|
@@ -972,6 +1052,9 @@ export function removePluginTools(names) {
|
|
|
972
1052
|
TOOL_REGISTRY.splice(i, 1);
|
|
973
1053
|
}
|
|
974
1054
|
}
|
|
1055
|
+
if (mcpServerInstance) {
|
|
1056
|
+
mcpServerInstance.server.sendToolListChanged().catch(() => { });
|
|
1057
|
+
}
|
|
975
1058
|
}
|
|
976
1059
|
export async function startMcpServer() {
|
|
977
1060
|
const server = new McpServer({
|
|
@@ -981,6 +1064,7 @@ export async function startMcpServer() {
|
|
|
981
1064
|
for (const tool of TOOL_REGISTRY) {
|
|
982
1065
|
server.tool(tool.name, tool.description, tool.schema, tool.handler);
|
|
983
1066
|
}
|
|
1067
|
+
mcpServerInstance = server;
|
|
984
1068
|
const transport = new StdioServerTransport();
|
|
985
1069
|
await server.connect(transport);
|
|
986
1070
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Router as createRouter } from 'express';
|
|
6
6
|
import { discoverPlugins, loadPluginModule } from './plugin-discovery.js';
|
|
7
7
|
import { registerPluginTools, removePluginTools } from './mcp.js';
|
|
8
|
-
import { readConfig, saveConfig } from './helpers.js';
|
|
8
|
+
import { readConfig, saveConfig, getDataDir } from './helpers.js';
|
|
9
9
|
import { broadcastPluginsChanged } from './ws.js';
|
|
10
10
|
export class PluginManager {
|
|
11
11
|
app;
|
|
@@ -55,7 +55,7 @@ export class PluginManager {
|
|
|
55
55
|
// Register routes via togglable middleware
|
|
56
56
|
if (plugin.registerRoutes) {
|
|
57
57
|
const router = createRouter();
|
|
58
|
-
await plugin.registerRoutes({ app: router, config: resolvedConfig });
|
|
58
|
+
await plugin.registerRoutes({ app: router, config: resolvedConfig, dataDir: getDataDir() });
|
|
59
59
|
managed.router = router;
|
|
60
60
|
// Wrap in middleware that skips when disabled
|
|
61
61
|
managed.middleware = (req, res, next) => {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler routes — proxy all requests to the platform API.
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import { platformFetch, isAuthenticated } from './connections.js';
|
|
6
|
+
export function createSchedulerRouter() {
|
|
7
|
+
const router = Router();
|
|
8
|
+
function proxy(path, method = 'GET') {
|
|
9
|
+
return async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
if (!isAuthenticated()) {
|
|
12
|
+
res.json({ error: 'Not authenticated' });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const options = { method };
|
|
16
|
+
if (method !== 'GET' && method !== 'DELETE') {
|
|
17
|
+
options.body = JSON.stringify(req.body);
|
|
18
|
+
}
|
|
19
|
+
const upstream = await platformFetch(path, options);
|
|
20
|
+
const data = await upstream.json();
|
|
21
|
+
if (!upstream.ok) {
|
|
22
|
+
res.status(upstream.status).json(data);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
res.json(data);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
res.status(500).json({ error: err.message });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// Slots
|
|
33
|
+
router.get('/api/scheduler/slots', proxy('/scheduler/slots'));
|
|
34
|
+
router.post('/api/scheduler/slots', proxy('/scheduler/slots', 'POST'));
|
|
35
|
+
router.patch('/api/scheduler/slots/:id', async (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
if (!isAuthenticated()) {
|
|
38
|
+
res.json({ error: 'Not authenticated' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const upstream = await platformFetch(`/scheduler/slots/${req.params.id}`, {
|
|
42
|
+
method: 'PATCH',
|
|
43
|
+
body: JSON.stringify(req.body),
|
|
44
|
+
});
|
|
45
|
+
const data = await upstream.json();
|
|
46
|
+
if (!upstream.ok) {
|
|
47
|
+
res.status(upstream.status).json(data);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
res.json(data);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
res.status(500).json({ error: err.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
router.delete('/api/scheduler/slots/:id', async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
if (!isAuthenticated()) {
|
|
59
|
+
res.json({ error: 'Not authenticated' });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const upstream = await platformFetch(`/scheduler/slots/${req.params.id}`, { method: 'DELETE' });
|
|
63
|
+
const data = await upstream.json();
|
|
64
|
+
if (!upstream.ok) {
|
|
65
|
+
res.status(upstream.status).json(data);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
res.json(data);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
res.status(500).json({ error: err.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Queue
|
|
75
|
+
router.get('/api/scheduler/queue', proxy('/scheduler/queue'));
|
|
76
|
+
router.post('/api/scheduler/queue', proxy('/scheduler/queue', 'POST'));
|
|
77
|
+
router.patch('/api/scheduler/queue/:id', async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
if (!isAuthenticated()) {
|
|
80
|
+
res.json({ error: 'Not authenticated' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const upstream = await platformFetch(`/scheduler/queue/${req.params.id}`, {
|
|
84
|
+
method: 'PATCH',
|
|
85
|
+
body: JSON.stringify(req.body),
|
|
86
|
+
});
|
|
87
|
+
const data = await upstream.json();
|
|
88
|
+
if (!upstream.ok) {
|
|
89
|
+
res.status(upstream.status).json(data);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
res.json(data);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
res.status(500).json({ error: err.message });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
router.delete('/api/scheduler/queue/:id', async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
if (!isAuthenticated()) {
|
|
101
|
+
res.json({ error: 'Not authenticated' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const upstream = await platformFetch(`/scheduler/queue/${req.params.id}`, { method: 'DELETE' });
|
|
105
|
+
const data = await upstream.json();
|
|
106
|
+
if (!upstream.ok) {
|
|
107
|
+
res.status(upstream.status).json(data);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
res.json(data);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
res.status(500).json({ error: err.message });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// History
|
|
117
|
+
router.get('/api/scheduler/history', proxy('/scheduler/history'));
|
|
118
|
+
// Available connections for scheduler
|
|
119
|
+
router.get('/api/scheduler/connections', proxy('/scheduler/connections'));
|
|
120
|
+
return router;
|
|
121
|
+
}
|