mulmoclaude 0.3.0 → 0.5.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/bin/mulmoclaude.js +7 -24
- package/client/assets/html2canvas-Cx501zZr-DiKaqnKs.js +5 -0
- package/client/assets/{index-eHWB79u5.js → index-C94GcmNa.js} +203 -198
- package/client/assets/index-CY-WpQUm.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-5ipqh8Pe.js} +5 -5
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/client/index.html +2 -4
- package/package.json +17 -15
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/backend/claude-code.ts +170 -0
- package/server/agent/backend/index.ts +14 -0
- package/server/agent/backend/types.ts +65 -0
- package/server/agent/index.ts +31 -159
- package/server/agent/mcp-server.ts +88 -10
- package/server/agent/mcp-tools/index.ts +8 -7
- package/server/agent/mcp-tools/notify.ts +76 -0
- package/server/agent/mcp-tools/x.ts +12 -2
- package/server/agent/plugin-names.ts +10 -4
- package/server/agent/prompt.ts +187 -26
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +3 -3
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +99 -4
- package/server/api/routes/chart.ts +13 -0
- package/server/api/routes/chat-index.ts +2 -1
- package/server/api/routes/config.ts +35 -8
- package/server/api/routes/files.ts +75 -24
- package/server/api/routes/html.ts +15 -2
- package/server/api/routes/image.ts +75 -20
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/news.ts +146 -0
- package/server/api/routes/notifications.ts +58 -2
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +73 -91
- package/server/api/routes/presentHtml.ts +9 -0
- package/server/api/routes/roles.ts +12 -2
- package/server/api/routes/scheduler.ts +20 -11
- package/server/api/routes/schedulerTasks.ts +58 -21
- package/server/api/routes/sessions.ts +15 -4
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +26 -5
- package/server/api/routes/sources.ts +8 -7
- package/server/api/routes/todos.ts +30 -0
- package/server/api/routes/todosColumnsHandlers.ts +13 -27
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki/frontmatter.ts +86 -0
- package/server/api/routes/wiki.ts +335 -75
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +32 -8
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +26 -16
- package/server/events/resolveRelayBridgeOptions.ts +125 -0
- package/server/index.ts +72 -49
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +15 -5
- package/server/system/macosNotify.ts +152 -0
- package/server/utils/errors.ts +11 -2
- package/server/utils/fetch.ts +54 -0
- package/server/utils/files/atomic.ts +18 -17
- package/server/utils/files/image-store.ts +19 -13
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-image-fill.ts +131 -0
- package/server/utils/files/markdown-store.ts +22 -6
- package/server/utils/files/naming.ts +20 -10
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +4 -4
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/spreadsheet-store.ts +15 -10
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +30 -4
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/id.ts +40 -8
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/logPreview.ts +24 -0
- package/server/utils/markdown.ts +5 -5
- package/server/utils/port.d.mts +6 -0
- package/server/utils/port.mjs +48 -0
- package/server/utils/promptMeta.ts +32 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/slug.ts +65 -4
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/index.ts +1 -1
- package/server/workspace/chat-index/summarizer.ts +5 -5
- package/server/workspace/custom-dirs.ts +5 -5
- package/server/workspace/helps/gemini.md +57 -0
- package/server/workspace/helps/index.md +2 -1
- package/server/workspace/helps/sources.md +42 -0
- package/server/workspace/helps/wiki.md +40 -5
- package/server/workspace/journal/archivist-cli.ts +121 -0
- package/server/workspace/journal/{archivist.ts → archivist-schemas.ts} +12 -120
- package/server/workspace/journal/dailyPass.ts +78 -38
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +56 -5
- package/server/workspace/journal/memoryExtractor.ts +1 -1
- package/server/workspace/journal/optimizationPass.ts +4 -5
- package/server/workspace/journal/paths.ts +8 -24
- package/server/workspace/journal/state.ts +18 -8
- package/server/workspace/news/reader.ts +248 -0
- package/server/workspace/paths.ts +4 -3
- package/server/workspace/reference-dirs.ts +3 -3
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +5 -4
- package/server/workspace/skills/user-tasks.ts +3 -2
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +2 -2
- package/server/workspace/sources/classifier.ts +1 -1
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +3 -3
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +59 -13
- package/server/workspace/sources/pipeline/index.ts +59 -7
- package/server/workspace/sources/pipeline/notify.ts +13 -5
- package/server/workspace/sources/pipeline/plan.ts +11 -9
- package/server/workspace/sources/pipeline/summarize.ts +1 -1
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/sourceState.ts +9 -4
- package/server/workspace/sources/types.ts +9 -0
- package/server/workspace/sources/urls.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +4 -4
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +477 -251
- package/src/components/CanvasViewToggle.vue +12 -10
- package/src/components/ChatInput.vue +112 -105
- package/src/components/FileContentHeader.vue +10 -7
- package/src/components/FileContentRenderer.vue +37 -10
- package/src/components/FileTree.vue +34 -4
- package/src/components/FileTreePane.vue +32 -27
- package/src/components/FilesView.vue +5 -3
- package/src/components/FilterChip.vue +22 -0
- package/src/components/LockStatusPopup.vue +19 -13
- package/src/components/NewsView.vue +252 -0
- package/src/components/NotificationBell.vue +35 -9
- package/src/components/NotificationToast.vue +4 -1
- package/src/components/PageChatComposer.vue +101 -0
- package/src/components/PluginLauncher.vue +36 -62
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +3 -2
- package/src/components/SessionHeaderControls.vue +63 -0
- package/src/components/SessionHistoryExpandButton.vue +30 -0
- package/src/components/SessionHistoryPanel.vue +64 -93
- package/src/components/SessionHistoryToggleButton.vue +40 -0
- package/src/components/SessionRoleIcon.vue +72 -0
- package/src/components/SessionSidebar.vue +96 -0
- package/src/components/SessionTabBar.vue +44 -51
- package/src/components/SettingsMcpTab.vue +361 -52
- package/src/components/SettingsModal.vue +203 -72
- package/src/components/SettingsReferenceDirsTab.vue +72 -51
- package/src/components/SettingsWorkspaceDirsTab.vue +74 -51
- package/src/components/SidebarHeader.vue +50 -16
- package/src/components/SourcesManager.vue +900 -0
- package/src/components/SourcesView.vue +45 -0
- package/src/components/StackView.vue +84 -48
- package/src/components/SuggestionsPanel.vue +25 -36
- package/src/components/SystemFileBanner.vue +106 -0
- package/src/components/ThinkingIndicator.vue +41 -0
- package/src/components/TodoExplorer.vue +72 -22
- package/src/components/todo/TodoAddDialog.vue +17 -12
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +15 -10
- package/src/components/todo/TodoKanbanView.vue +16 -6
- package/src/components/todo/TodoListView.vue +14 -3
- package/src/components/todo/TodoTableView.vue +36 -5
- package/src/composables/favicon/conditions.ts +76 -0
- package/src/composables/favicon/resolveColor.ts +93 -0
- package/src/composables/favicon/types.ts +61 -0
- package/src/composables/useAppApi.ts +23 -0
- package/src/composables/useChatScroll.ts +5 -5
- package/src/composables/useCurrentRole.ts +32 -0
- package/src/composables/useDynamicFavicon.ts +174 -58
- package/src/composables/useEventListeners.ts +7 -12
- package/src/composables/useFaviconState.ts +93 -12
- package/src/composables/useFileSelection.ts +25 -6
- package/src/composables/useHealth.ts +76 -7
- package/src/composables/useLayoutMode.ts +27 -0
- package/src/composables/useNewsItems.ts +38 -0
- package/src/composables/useNewsReadState.ts +75 -0
- package/src/composables/useNotifications.ts +76 -13
- package/src/composables/usePendingCalls.ts +11 -1
- package/src/composables/useRoles.ts +6 -10
- package/src/composables/useRunElapsed.ts +80 -0
- package/src/composables/useSessionDerived.ts +21 -5
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSidePanelVisible.ts +25 -0
- package/src/composables/useViewLayout.ts +16 -37
- package/src/config/apiRoutes.ts +19 -6
- package/src/config/historyFilters.ts +30 -0
- package/src/config/mcpCatalog.ts +285 -0
- package/src/config/mcpTypes.ts +26 -0
- package/src/config/roles.ts +19 -51
- package/src/config/systemFileDescriptors.ts +170 -0
- package/src/config/toolNames.ts +6 -1
- package/src/config/workspacePaths.ts +1 -0
- package/src/index.css +14 -0
- package/src/lang/de.ts +706 -0
- package/src/lang/en.ts +726 -0
- package/src/lang/es.ts +712 -0
- package/src/lang/fr.ts +704 -0
- package/src/lang/ja.ts +707 -0
- package/src/lang/ko.ts +709 -0
- package/src/lang/pt-BR.ts +702 -0
- package/src/lang/zh.ts +705 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +3 -0
- package/src/plugins/canvas/View.vue +104 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/canvas/index.ts +3 -2
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/chart/index.ts +3 -2
- package/src/plugins/editImage/index.ts +3 -2
- package/src/plugins/generateImage/index.ts +3 -2
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +67 -46
- package/src/plugins/manageRoles/index.ts +3 -2
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +39 -34
- package/src/plugins/manageSkills/index.ts +3 -2
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/manageSource/View.vue +3 -687
- package/src/plugins/manageSource/index.ts +3 -2
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +164 -73
- package/src/plugins/markdown/definition.ts +6 -4
- package/src/plugins/markdown/index.ts +3 -2
- package/src/plugins/presentForm/Preview.vue +99 -0
- package/src/plugins/presentForm/View.vue +675 -0
- package/src/plugins/presentForm/definition.ts +127 -0
- package/src/plugins/presentForm/index.ts +18 -0
- package/src/plugins/presentForm/plugin.ts +94 -0
- package/src/plugins/presentForm/types.ts +109 -0
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/index.ts +3 -2
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +36 -26
- package/src/plugins/presentMulmoScript/index.ts +3 -2
- package/src/plugins/scheduler/AutomationsPreview.vue +37 -0
- package/src/plugins/scheduler/AutomationsView.vue +23 -0
- package/src/plugins/scheduler/CalendarView.vue +23 -0
- package/src/plugins/scheduler/LegacySchedulerView.vue +32 -0
- package/src/plugins/scheduler/Preview.vue +7 -4
- package/src/plugins/scheduler/TasksTab.vue +119 -28
- package/src/plugins/scheduler/View.vue +75 -32
- package/src/plugins/scheduler/automationsDefinition.ts +58 -0
- package/src/plugins/scheduler/calendarDefinition.ts +46 -0
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/scheduler/index.ts +68 -14
- package/src/plugins/scheduler/legacyShape.ts +34 -0
- package/src/plugins/spreadsheet/Preview.vue +9 -5
- package/src/plugins/spreadsheet/View.vue +43 -57
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +2 -1
- package/src/plugins/spreadsheet/index.ts +3 -2
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +42 -45
- package/src/plugins/textResponse/utils.ts +25 -0
- package/src/plugins/todo/Preview.vue +11 -6
- package/src/plugins/todo/View.vue +27 -13
- package/src/plugins/todo/composables/useTodos.ts +3 -1
- package/src/plugins/todo/index.ts +3 -2
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +5 -2
- package/src/plugins/wiki/View.vue +539 -92
- package/src/plugins/wiki/index.ts +5 -2
- package/src/plugins/wiki/route.ts +121 -0
- package/src/router/guards.ts +43 -24
- package/src/router/index.ts +53 -26
- package/src/router/pageRoutes.ts +23 -0
- package/src/tools/index.ts +12 -5
- package/src/tools/legacyPluginNames.ts +13 -0
- package/src/types/notification.ts +31 -6
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/eventDispatch.ts +3 -6
- package/src/utils/agent/formatElapsed.ts +37 -0
- package/src/utils/agent/request.ts +22 -1
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/canvas/sidePanelVisible.ts +19 -0
- package/src/utils/dom/scrollIntoViewByTestId.ts +38 -0
- package/src/utils/errors.ts +9 -2
- package/src/utils/files/filename.ts +24 -0
- package/src/utils/filesPreview/schedulerPreview.ts +9 -3
- package/src/utils/id.ts +18 -0
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/markdown/taskList.ts +175 -0
- package/src/utils/mcp/interpolateSpec.ts +97 -0
- package/src/utils/notification/dispatch.ts +51 -15
- package/src/utils/path/workspaceLinkRouter.ts +99 -0
- package/src/utils/session/mergeSessions.ts +5 -0
- package/src/utils/sources/filter.ts +69 -0
- package/src/vite-env.d.ts +9 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
- package/client/assets/index-Bm70FDU2.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/server/workspace/journal/linkRewrite.ts +0 -4
- package/src/components/ToolResultsPanel.vue +0 -77
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/plugins/scheduler/definition.ts +0 -57
- package/src/utils/canvas/viewMode.ts +0 -46
- package/src/utils/role/plugins.ts +0 -12
- package/src/utils/session/seedRoleDefault.ts +0 -35
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Router, Request, Response } from "express";
|
|
2
|
-
import
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { WORKSPACE_PATHS } from "../../workspace/paths.js";
|
|
5
5
|
import { stripDataUri } from "../../utils/files/image-store.js";
|
|
6
|
+
import { writeJsonAtomic } from "../../utils/files/json.js";
|
|
6
7
|
import {
|
|
7
8
|
getFileObject,
|
|
8
9
|
initializeContextFromFiles,
|
|
@@ -26,6 +27,7 @@ import { slugify } from "../../utils/slug.js";
|
|
|
26
27
|
import { resolveWithinRoot } from "../../utils/files/safe.js";
|
|
27
28
|
import { errorMessage } from "../../utils/errors.js";
|
|
28
29
|
import { badRequest, notFound, serverError } from "../../utils/httpError.js";
|
|
30
|
+
import { getOptionalStringQuery } from "../../utils/request.js";
|
|
29
31
|
import { log } from "../../system/logger/index.js";
|
|
30
32
|
import { validateUpdateBeatBody, validateUpdateScriptBody } from "./mulmoScriptValidate.js";
|
|
31
33
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
@@ -54,8 +56,8 @@ let storiesRealCache: string | null = null;
|
|
|
54
56
|
function ensureStoriesReal(): string | null {
|
|
55
57
|
if (storiesRealCache) return storiesRealCache;
|
|
56
58
|
try {
|
|
57
|
-
|
|
58
|
-
storiesRealCache =
|
|
59
|
+
mkdirSync(storiesDir, { recursive: true });
|
|
60
|
+
storiesRealCache = realpathSync(storiesDir);
|
|
59
61
|
return storiesRealCache;
|
|
60
62
|
} catch {
|
|
61
63
|
return null;
|
|
@@ -96,7 +98,7 @@ interface FilePathQuery {
|
|
|
96
98
|
filePath?: string;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
router.post(API_ROUTES.mulmoScript.save, (req: Request<object, object, SaveMulmoScriptBody>, res: Response) => {
|
|
101
|
+
router.post(API_ROUTES.mulmoScript.save, async (req: Request<object, object, SaveMulmoScriptBody>, res: Response) => {
|
|
100
102
|
const { script, filename } = req.body;
|
|
101
103
|
|
|
102
104
|
if (!script || !Array.isArray(script.beats)) {
|
|
@@ -104,14 +106,14 @@ router.post(API_ROUTES.mulmoScript.save, (req: Request<object, object, SaveMulmo
|
|
|
104
106
|
return;
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
|
|
109
|
+
mkdirSync(storiesDir, { recursive: true });
|
|
108
110
|
|
|
109
111
|
const title = script.title || "untitled";
|
|
110
112
|
const slug = filename ? filename.replace(/\.json$/, "") : slugify(title);
|
|
111
113
|
const fname = `${slug}-${Date.now()}.json`;
|
|
112
114
|
const filePath = path.join(storiesDir, fname);
|
|
113
115
|
|
|
114
|
-
|
|
116
|
+
await writeJsonAtomic(filePath, script);
|
|
115
117
|
|
|
116
118
|
res.json({
|
|
117
119
|
data: { script, filePath: `stories/${fname}` },
|
|
@@ -120,7 +122,7 @@ router.post(API_ROUTES.mulmoScript.save, (req: Request<object, object, SaveMulmo
|
|
|
120
122
|
});
|
|
121
123
|
});
|
|
122
124
|
|
|
123
|
-
router.post(API_ROUTES.mulmoScript.updateBeat, (req: Request<object, object, unknown>, res: Response) => {
|
|
125
|
+
router.post(API_ROUTES.mulmoScript.updateBeat, async (req: Request<object, object, unknown>, res: Response) => {
|
|
124
126
|
const validation = validateUpdateBeatBody(req.body);
|
|
125
127
|
if (!validation.ok) {
|
|
126
128
|
badRequest(res, validation.error);
|
|
@@ -131,7 +133,7 @@ router.post(API_ROUTES.mulmoScript.updateBeat, (req: Request<object, object, unk
|
|
|
131
133
|
const absoluteFilePath = resolveStoryPath(filePath, res);
|
|
132
134
|
if (!absoluteFilePath) return;
|
|
133
135
|
|
|
134
|
-
const script: MulmoScript = JSON.parse(
|
|
136
|
+
const script: MulmoScript = JSON.parse(readFileSync(absoluteFilePath, "utf-8"));
|
|
135
137
|
|
|
136
138
|
if (!Array.isArray(script.beats) || beatIndex >= script.beats.length) {
|
|
137
139
|
badRequest(res, "Invalid beatIndex");
|
|
@@ -139,12 +141,12 @@ router.post(API_ROUTES.mulmoScript.updateBeat, (req: Request<object, object, unk
|
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
script.beats[beatIndex] = beat as MulmoBeat;
|
|
142
|
-
|
|
144
|
+
await writeJsonAtomic(absoluteFilePath, script);
|
|
143
145
|
|
|
144
146
|
res.json({ ok: true });
|
|
145
147
|
});
|
|
146
148
|
|
|
147
|
-
router.post(API_ROUTES.mulmoScript.updateScript, (req: Request<object, object, unknown>, res: Response) => {
|
|
149
|
+
router.post(API_ROUTES.mulmoScript.updateScript, async (req: Request<object, object, unknown>, res: Response) => {
|
|
148
150
|
const validation = validateUpdateScriptBody(req.body);
|
|
149
151
|
if (!validation.ok) {
|
|
150
152
|
badRequest(res, validation.error);
|
|
@@ -155,7 +157,7 @@ router.post(API_ROUTES.mulmoScript.updateScript, (req: Request<object, object, u
|
|
|
155
157
|
const absoluteFilePath = resolveStoryPath(filePath, res);
|
|
156
158
|
if (!absoluteFilePath) return;
|
|
157
159
|
|
|
158
|
-
|
|
160
|
+
await writeJsonAtomic(absoluteFilePath, updatedScript);
|
|
159
161
|
res.json({ ok: true });
|
|
160
162
|
});
|
|
161
163
|
|
|
@@ -170,7 +172,7 @@ router.get(API_ROUTES.mulmoScript.beatImage, async (req: Request<object, BeatIma
|
|
|
170
172
|
|
|
171
173
|
await withStoryContext(res, filePath, {}, async ({ context }) => {
|
|
172
174
|
const { imagePath } = getBeatPngImagePath(context, beatIndex);
|
|
173
|
-
if (!
|
|
175
|
+
if (!existsSync(imagePath)) {
|
|
174
176
|
res.json({ image: null });
|
|
175
177
|
return;
|
|
176
178
|
}
|
|
@@ -197,13 +199,13 @@ router.get(API_ROUTES.mulmoScript.movieStatus, async (req: Request<object, Movie
|
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
const outputPath = movieFilePath(context);
|
|
200
|
-
if (!
|
|
202
|
+
if (!existsSync(outputPath)) {
|
|
201
203
|
res.json({ moviePath: null });
|
|
202
204
|
return;
|
|
203
205
|
}
|
|
204
206
|
|
|
205
|
-
const movieMtime =
|
|
206
|
-
const sourceMtime =
|
|
207
|
+
const movieMtime = statSync(outputPath).mtimeMs;
|
|
208
|
+
const sourceMtime = statSync(absoluteFilePath).mtimeMs;
|
|
207
209
|
if (movieMtime < sourceMtime) {
|
|
208
210
|
res.json({ moviePath: null });
|
|
209
211
|
return;
|
|
@@ -216,7 +218,7 @@ router.get(API_ROUTES.mulmoScript.movieStatus, async (req: Request<object, Movie
|
|
|
216
218
|
});
|
|
217
219
|
|
|
218
220
|
function fileToDataUri(filePath: string, mimeType: string): string {
|
|
219
|
-
const data =
|
|
221
|
+
const data = readFileSync(filePath);
|
|
220
222
|
return `data:${mimeType};base64,${data.toString("base64")}`;
|
|
221
223
|
}
|
|
222
224
|
|
|
@@ -257,7 +259,7 @@ function resolveStoryPath(filePath: string, res: Response): string | null {
|
|
|
257
259
|
const resolved = resolveWithinRoot(storiesReal, relFromStories);
|
|
258
260
|
if (!resolved) {
|
|
259
261
|
const candidate = path.resolve(storiesReal, relFromStories);
|
|
260
|
-
if (!
|
|
262
|
+
if (!existsSync(candidate)) {
|
|
261
263
|
notFound(res, `File not found: ${filePath}`);
|
|
262
264
|
} else {
|
|
263
265
|
badRequest(res, "Invalid filePath");
|
|
@@ -373,12 +375,12 @@ router.get(API_ROUTES.mulmoScript.beatAudio, async (req: Request<object, BeatAud
|
|
|
373
375
|
filePath,
|
|
374
376
|
{
|
|
375
377
|
operation: "beat-audio",
|
|
376
|
-
onContextMissing: (
|
|
378
|
+
onContextMissing: (response) => response.json({ audio: null }),
|
|
377
379
|
},
|
|
378
380
|
async ({ context }) => {
|
|
379
381
|
const beat = context.studio.script.beats[beatIndex];
|
|
380
382
|
const audioPath = getBeatAudioPathOrUrl(beat.text ?? "", context, beat, context.lang);
|
|
381
|
-
if (!audioPath || !
|
|
383
|
+
if (!audioPath || !existsSync(audioPath)) {
|
|
382
384
|
res.json({ audio: null });
|
|
383
385
|
return;
|
|
384
386
|
}
|
|
@@ -422,7 +424,7 @@ router.post(
|
|
|
422
424
|
const beat = context.studio.script.beats[beatIndex];
|
|
423
425
|
const audioPath = context.studio.beats[beatIndex]?.audioFile ?? getBeatAudioPathOrUrl(beat.text ?? "", context, beat, context.lang);
|
|
424
426
|
|
|
425
|
-
if (!audioPath || !
|
|
427
|
+
if (!audioPath || !existsSync(audioPath)) {
|
|
426
428
|
// Logic-flow failure (not an exception) — emit a targeted
|
|
427
429
|
// log. Don't write raw `beat.text` into persistent logs —
|
|
428
430
|
// it's free-form user content and can contain sensitive
|
|
@@ -430,7 +432,7 @@ router.post(
|
|
|
430
432
|
log.error("generate-beat-audio", "audio was not generated", {
|
|
431
433
|
beatIndex,
|
|
432
434
|
audioPath,
|
|
433
|
-
exists: audioPath ?
|
|
435
|
+
exists: audioPath ? existsSync(audioPath) : false,
|
|
434
436
|
beatTextLength: typeof beat?.text === "string" ? beat.text.length : 0,
|
|
435
437
|
audioFilePresent: Boolean(context.studio.beats[beatIndex]?.audioFile),
|
|
436
438
|
});
|
|
@@ -475,7 +477,7 @@ router.post(API_ROUTES.mulmoScript.renderBeat, async (req: Request<object, objec
|
|
|
475
477
|
});
|
|
476
478
|
|
|
477
479
|
const { imagePath } = getBeatPngImagePath(context, beatIndex);
|
|
478
|
-
if (!
|
|
480
|
+
if (!existsSync(imagePath)) {
|
|
479
481
|
genError = "Image was not generated";
|
|
480
482
|
serverError(res, genError);
|
|
481
483
|
return;
|
|
@@ -542,7 +544,7 @@ router.post(API_ROUTES.mulmoScript.generateMovie, async (req: Request<object, ob
|
|
|
542
544
|
await movie(audioContext);
|
|
543
545
|
|
|
544
546
|
const outputPath = movieFilePath(audioContext);
|
|
545
|
-
if (!
|
|
547
|
+
if (!existsSync(outputPath)) {
|
|
546
548
|
genError = "Movie was not generated";
|
|
547
549
|
send({ type: "error", message: genError });
|
|
548
550
|
res.end();
|
|
@@ -594,7 +596,7 @@ router.get(
|
|
|
594
596
|
|
|
595
597
|
await withStoryContext(res, filePath, {}, async ({ context }) => {
|
|
596
598
|
const imagePath = getReferenceImagePath(context, key, "png");
|
|
597
|
-
if (!
|
|
599
|
+
if (!existsSync(imagePath)) {
|
|
598
600
|
res.json({ image: null });
|
|
599
601
|
return;
|
|
600
602
|
}
|
|
@@ -613,10 +615,10 @@ router.post(API_ROUTES.mulmoScript.uploadBeatImage, async (req: Request<object,
|
|
|
613
615
|
|
|
614
616
|
await withStoryContext(res, filePath, {}, async ({ context }) => {
|
|
615
617
|
const { imagePath } = getBeatPngImagePath(context, beatIndex);
|
|
616
|
-
|
|
618
|
+
mkdirSync(path.dirname(imagePath), { recursive: true });
|
|
617
619
|
|
|
618
620
|
const base64 = stripDataUri(imageData);
|
|
619
|
-
|
|
621
|
+
writeFileSync(imagePath, Buffer.from(base64, "base64"));
|
|
620
622
|
|
|
621
623
|
res.json({ image: fileToDataUri(imagePath, "image/png") });
|
|
622
624
|
});
|
|
@@ -647,7 +649,7 @@ router.post(
|
|
|
647
649
|
|
|
648
650
|
const index = Object.keys(images).indexOf(key);
|
|
649
651
|
const imagePath = getReferenceImagePath(context, key, "png");
|
|
650
|
-
|
|
652
|
+
mkdirSync(path.dirname(imagePath), { recursive: true });
|
|
651
653
|
|
|
652
654
|
await generateReferenceImage({
|
|
653
655
|
context,
|
|
@@ -656,7 +658,7 @@ router.post(
|
|
|
656
658
|
image: imageEntry as MulmoImagePromptMedia,
|
|
657
659
|
force,
|
|
658
660
|
});
|
|
659
|
-
if (!
|
|
661
|
+
if (!existsSync(imagePath)) {
|
|
660
662
|
genError = "Character image was not generated";
|
|
661
663
|
serverError(res, genError);
|
|
662
664
|
return;
|
|
@@ -685,10 +687,10 @@ router.post(
|
|
|
685
687
|
|
|
686
688
|
await withStoryContext(res, filePath, {}, async ({ context }) => {
|
|
687
689
|
const imagePath = getReferenceImagePath(context, key, "png");
|
|
688
|
-
|
|
690
|
+
mkdirSync(path.dirname(imagePath), { recursive: true });
|
|
689
691
|
|
|
690
692
|
const base64 = stripDataUri(imageData);
|
|
691
|
-
|
|
693
|
+
writeFileSync(imagePath, Buffer.from(base64, "base64"));
|
|
692
694
|
|
|
693
695
|
res.json({ image: fileToDataUri(imagePath, "image/png") });
|
|
694
696
|
});
|
|
@@ -696,7 +698,7 @@ router.post(
|
|
|
696
698
|
);
|
|
697
699
|
|
|
698
700
|
router.get(API_ROUTES.mulmoScript.downloadMovie, (req: Request, res: Response) => {
|
|
699
|
-
const moviePath =
|
|
701
|
+
const moviePath = getOptionalStringQuery(req, "moviePath");
|
|
700
702
|
|
|
701
703
|
if (!moviePath) {
|
|
702
704
|
badRequest(res, "moviePath is required");
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// HTTP routes for the news viewer (#761).
|
|
2
|
+
//
|
|
3
|
+
// GET /api/news/items?days=N — recent items, sorted desc
|
|
4
|
+
// GET /api/news/items/:id/body — body markdown from per-source archive
|
|
5
|
+
// GET /api/news/read-state — { readIds: string[] }
|
|
6
|
+
// PUT /api/news/read-state — { readIds: string[] }
|
|
7
|
+
//
|
|
8
|
+
// The reader walks daily JSON indexes on demand (see
|
|
9
|
+
// `server/workspace/news/reader.ts`); no separate index is
|
|
10
|
+
// maintained — see issue #761 for the design rationale (covers ~30
|
|
11
|
+
// days, follow-up ticket for full history).
|
|
12
|
+
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { Router, type Request, type Response } from "express";
|
|
15
|
+
import { workspacePath } from "../../workspace/workspace.js";
|
|
16
|
+
import { aggregateRecentItems, loadItemBody, type NewsItem } from "../../workspace/news/reader.js";
|
|
17
|
+
import { loadJsonFile, writeJsonAtomic } from "../../utils/files/json.js";
|
|
18
|
+
import { WORKSPACE_FILES } from "../../../src/config/workspacePaths.js";
|
|
19
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
20
|
+
import { badRequest, serverError } from "../../utils/httpError.js";
|
|
21
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
22
|
+
import { isRecord } from "../../utils/types.js";
|
|
23
|
+
import { log } from "../../system/logger/index.js";
|
|
24
|
+
|
|
25
|
+
// Window upper bound — keeps memory bounded if a caller passes
|
|
26
|
+
// a comically large `days`. Daily JSON aggregation is O(days * items)
|
|
27
|
+
// in the worst case.
|
|
28
|
+
const MAX_DAYS = 90;
|
|
29
|
+
const DEFAULT_DAYS = 30;
|
|
30
|
+
|
|
31
|
+
// Cap the read-state list size. UUID-shaped ids are ~16 bytes each;
|
|
32
|
+
// 10k of them is < 200 KB, fine. Beyond that we evict from the front
|
|
33
|
+
// (oldest mark-as-read first) so the working set stays small.
|
|
34
|
+
const MAX_READ_IDS = 10_000;
|
|
35
|
+
|
|
36
|
+
interface ReadState {
|
|
37
|
+
readIds: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const router = Router();
|
|
41
|
+
|
|
42
|
+
// ── /api/news/items ─────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
router.get(API_ROUTES.news.items, async (req: Request, res: Response<{ items: NewsItem[] } | { error: string }>) => {
|
|
45
|
+
const days = parseDays(req.query.days);
|
|
46
|
+
if (days === null) {
|
|
47
|
+
badRequest(res, "invalid `days` query parameter");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const items = await aggregateRecentItems(workspacePath, days);
|
|
52
|
+
res.json({ items });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
log.error("news", "aggregate failed", { error: errorMessage(err) });
|
|
55
|
+
serverError(res, "failed to load news items");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function parseDays(raw: unknown): number | null {
|
|
60
|
+
if (raw === undefined) return DEFAULT_DAYS;
|
|
61
|
+
const value = Number(Array.isArray(raw) ? raw[0] : raw);
|
|
62
|
+
if (!Number.isInteger(value) || value <= 0) return null;
|
|
63
|
+
return Math.min(value, MAX_DAYS);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── /api/news/items/:id/body ─────────────────────────────────────
|
|
67
|
+
//
|
|
68
|
+
// Body lookup needs the item's `sourceSlug` + `publishedAt` to
|
|
69
|
+
// pick the right archive file. We re-aggregate on demand (cheap;
|
|
70
|
+
// could be cached later if it shows up in a hot path). The
|
|
71
|
+
// frontend has the metadata already, but routing it through query
|
|
72
|
+
// params would let an attacker fabricate paths — so we resolve from
|
|
73
|
+
// the trusted in-memory aggregate keyed by `id` here.
|
|
74
|
+
|
|
75
|
+
router.get(API_ROUTES.news.itemBody, async (req: Request<{ id: string }>, res: Response<{ body: string | null } | { error: string }>) => {
|
|
76
|
+
const { id } = req.params;
|
|
77
|
+
if (!id) {
|
|
78
|
+
badRequest(res, "missing item id");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const items = await aggregateRecentItems(workspacePath, MAX_DAYS);
|
|
83
|
+
const match = items.find((entry) => entry.id === id);
|
|
84
|
+
if (!match) {
|
|
85
|
+
res.json({ body: null });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const body = await loadItemBody(workspacePath, match.sourceSlug, match.url, match.publishedAt);
|
|
89
|
+
res.json({ body });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
log.error("news", "body lookup failed", { error: errorMessage(err) });
|
|
92
|
+
serverError(res, "failed to load item body");
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── /api/news/read-state ─────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
const readStateAbsPath = (): string => path.join(workspacePath, WORKSPACE_FILES.newsReadState);
|
|
99
|
+
|
|
100
|
+
router.get(API_ROUTES.news.readState, (_req: Request, res: Response<ReadState | { error: string }>) => {
|
|
101
|
+
try {
|
|
102
|
+
const data = loadJsonFile<ReadState>(readStateAbsPath(), { readIds: [] });
|
|
103
|
+
const sanitized = sanitizeReadState(data);
|
|
104
|
+
res.json(sanitized);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
log.error("news", "read-state load failed", {
|
|
107
|
+
error: errorMessage(err),
|
|
108
|
+
});
|
|
109
|
+
serverError(res, "failed to load news read-state");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
router.put(API_ROUTES.news.readState, async (req: Request, res: Response<ReadState | { error: string }>) => {
|
|
114
|
+
const body = req.body;
|
|
115
|
+
if (!isRecord(body) || !Array.isArray(body.readIds)) {
|
|
116
|
+
badRequest(res, "expected { readIds: string[] }");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const sanitized = sanitizeReadState({ readIds: body.readIds });
|
|
120
|
+
try {
|
|
121
|
+
await writeJsonAtomic(readStateAbsPath(), sanitized);
|
|
122
|
+
res.json(sanitized);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
log.error("news", "read-state save failed", {
|
|
125
|
+
error: errorMessage(err),
|
|
126
|
+
});
|
|
127
|
+
serverError(res, "failed to save news read-state");
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Drop non-string entries, dedupe, cap at MAX_READ_IDS (keeping the
|
|
132
|
+
// most recent — i.e. tail end — of the list). Pure for testability.
|
|
133
|
+
export function sanitizeReadState(input: { readIds: unknown[] }): ReadState {
|
|
134
|
+
const seen = new Set<string>();
|
|
135
|
+
const ordered: string[] = [];
|
|
136
|
+
for (const value of input.readIds) {
|
|
137
|
+
if (typeof value !== "string" || value.length === 0) continue;
|
|
138
|
+
if (seen.has(value)) continue;
|
|
139
|
+
seen.add(value);
|
|
140
|
+
ordered.push(value);
|
|
141
|
+
}
|
|
142
|
+
if (ordered.length <= MAX_READ_IDS) return { readIds: ordered };
|
|
143
|
+
return { readIds: ordered.slice(ordered.length - MAX_READ_IDS) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default router;
|
|
@@ -2,15 +2,27 @@
|
|
|
2
2
|
// simultaneously to every open Web tab (pub-sub) and every bridge
|
|
3
3
|
// (chat-service `pushToBridge`). Stepping stone for the in-app
|
|
4
4
|
// notification center (#144) and external-channel notifications
|
|
5
|
-
// (#142); see plans/feat-notification-push-scaffold.md for the
|
|
5
|
+
// (#142); see plans/done/feat-notification-push-scaffold.md for the
|
|
6
6
|
// motivation and the production plan.
|
|
7
7
|
//
|
|
8
|
-
// Usage:
|
|
8
|
+
// Usage (basic):
|
|
9
9
|
// curl -X POST http://localhost:3001/api/notifications/test \
|
|
10
10
|
// -H "Authorization: Bearer $(cat ~/mulmoclaude/.session-token)" \
|
|
11
11
|
// -H "Content-Type: application/json" \
|
|
12
12
|
// -d '{"message":"hello","delaySeconds":5}'
|
|
13
13
|
//
|
|
14
|
+
// Usage (with permalink action — #762):
|
|
15
|
+
// curl -X POST http://localhost:3001/api/notifications/test \
|
|
16
|
+
// -H "Authorization: Bearer $TOKEN" \
|
|
17
|
+
// -H "Content-Type: application/json" \
|
|
18
|
+
// -d '{"message":"Scheduled task fired","kind":"scheduler",
|
|
19
|
+
// "action":{"type":"navigate",
|
|
20
|
+
// "target":{"view":"automations",
|
|
21
|
+
// "taskId":"finance-daily-briefing"}}}'
|
|
22
|
+
//
|
|
23
|
+
// For a one-shot "fire one of every target kind" run,
|
|
24
|
+
// scripts/dev/fire-sample-notifications.sh drives this endpoint.
|
|
25
|
+
//
|
|
14
26
|
// The route is exported as a factory so the host wiring can inject
|
|
15
27
|
// the pub-sub publisher and the chat-service push handle without
|
|
16
28
|
// this file pulling in either module directly.
|
|
@@ -19,12 +31,23 @@ import { Router, type Request, type Response } from "express";
|
|
|
19
31
|
import { scheduleTestNotification, type NotificationDeps, type ScheduleNotificationOptions } from "../../events/notifications.js";
|
|
20
32
|
import { log } from "../../system/logger/index.js";
|
|
21
33
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
34
|
+
import {
|
|
35
|
+
NOTIFICATION_ACTION_TYPES,
|
|
36
|
+
NOTIFICATION_KINDS,
|
|
37
|
+
NOTIFICATION_VIEWS,
|
|
38
|
+
type NotificationAction,
|
|
39
|
+
type NotificationKind,
|
|
40
|
+
} from "../../../src/types/notification.js";
|
|
41
|
+
import { isRecord } from "../../utils/types.js";
|
|
22
42
|
|
|
23
43
|
interface TestRequestBody {
|
|
24
44
|
message?: unknown;
|
|
45
|
+
body?: unknown;
|
|
25
46
|
delaySeconds?: unknown;
|
|
26
47
|
transportId?: unknown;
|
|
27
48
|
chatId?: unknown;
|
|
49
|
+
kind?: unknown;
|
|
50
|
+
action?: unknown;
|
|
28
51
|
}
|
|
29
52
|
|
|
30
53
|
interface TestResponse {
|
|
@@ -32,11 +55,40 @@ interface TestResponse {
|
|
|
32
55
|
delaySeconds: number;
|
|
33
56
|
}
|
|
34
57
|
|
|
58
|
+
const KIND_SET = new Set<string>(Object.values(NOTIFICATION_KINDS));
|
|
59
|
+
const VIEW_SET = new Set<string>(Object.values(NOTIFICATION_VIEWS));
|
|
60
|
+
|
|
61
|
+
function parseKind(value: unknown): NotificationKind | undefined {
|
|
62
|
+
if (typeof value !== "string") return undefined;
|
|
63
|
+
return KIND_SET.has(value) ? (value as NotificationKind) : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Loose validator — dev-facing so we prefer "pass through what looks
|
|
67
|
+
// right" over strict type guards. Only checks that `type` is one of
|
|
68
|
+
// the two known literals and, for navigates, that `target.view` is a
|
|
69
|
+
// known view. The target's other fields are forwarded verbatim so
|
|
70
|
+
// adding new optional fields doesn't require editing this file.
|
|
71
|
+
function parseAction(value: unknown): NotificationAction | undefined {
|
|
72
|
+
if (!isRecord(value)) return undefined;
|
|
73
|
+
if (value.type === NOTIFICATION_ACTION_TYPES.none) {
|
|
74
|
+
return { type: NOTIFICATION_ACTION_TYPES.none };
|
|
75
|
+
}
|
|
76
|
+
if (value.type !== NOTIFICATION_ACTION_TYPES.navigate) return undefined;
|
|
77
|
+
const target = value.target;
|
|
78
|
+
if (!isRecord(target) || typeof target.view !== "string" || !VIEW_SET.has(target.view)) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return { type: NOTIFICATION_ACTION_TYPES.navigate, target: target as NotificationAction extends { target: infer T } ? T : never };
|
|
82
|
+
}
|
|
83
|
+
|
|
35
84
|
function parseBody(body: TestRequestBody): ScheduleNotificationOptions {
|
|
36
85
|
const opts: ScheduleNotificationOptions = {};
|
|
37
86
|
if (typeof body.message === "string" && body.message.length > 0) {
|
|
38
87
|
opts.message = body.message;
|
|
39
88
|
}
|
|
89
|
+
if (typeof body.body === "string" && body.body.length > 0) {
|
|
90
|
+
opts.body = body.body;
|
|
91
|
+
}
|
|
40
92
|
if (typeof body.delaySeconds === "number") {
|
|
41
93
|
opts.delaySeconds = body.delaySeconds;
|
|
42
94
|
}
|
|
@@ -46,6 +98,10 @@ function parseBody(body: TestRequestBody): ScheduleNotificationOptions {
|
|
|
46
98
|
if (typeof body.chatId === "string" && body.chatId.length > 0) {
|
|
47
99
|
opts.chatId = body.chatId;
|
|
48
100
|
}
|
|
101
|
+
const kind = parseKind(body.kind);
|
|
102
|
+
if (kind) opts.kind = kind;
|
|
103
|
+
const action = parseAction(body.action);
|
|
104
|
+
if (action) opts.action = action;
|
|
49
105
|
return opts;
|
|
50
106
|
}
|
|
51
107
|
|
package/server/api/routes/pdf.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { realpathSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { Router, Request, Response } from "express";
|
|
4
4
|
import { marked } from "marked";
|
|
@@ -56,7 +56,7 @@ const MIME_BY_EXT: Record<string, string> = {
|
|
|
56
56
|
// Realpath of the workspace, resolved once at module load. Used to
|
|
57
57
|
// validate that image paths resolved relative to markdowns/ stay
|
|
58
58
|
// inside the workspace after symlink resolution.
|
|
59
|
-
const workspaceReal =
|
|
59
|
+
const workspaceReal = realpathSync(resolveWorkspacePath(""));
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Inline local images as base64 data URIs so Puppeteer can render them.
|