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,18 +1,26 @@
|
|
|
1
1
|
import { Router, Request, Response } from "express";
|
|
2
|
-
import
|
|
2
|
+
import { ReadStream, Stats, createReadStream, readFileSync, realpathSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { workspacePath } from "../../workspace/workspace.js";
|
|
5
5
|
import { statSafe, statSafeAsync, readDirSafeAsync, resolveWithinRoot, writeFileAtomic } from "../../utils/files/index.js";
|
|
6
6
|
import { errorMessage } from "../../utils/errors.js";
|
|
7
7
|
import { badRequest, notFound, sendError, serverError } from "../../utils/httpError.js";
|
|
8
|
+
import { getOptionalStringQuery } from "../../utils/request.js";
|
|
8
9
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
9
10
|
import { GitignoreFilter } from "../../utils/gitignore.js";
|
|
10
11
|
import { getCachedReferenceDirs } from "../../workspace/reference-dirs.js";
|
|
12
|
+
import { log } from "../../system/logger/index.js";
|
|
13
|
+
import { previewSnippet } from "../../utils/logPreview.js";
|
|
11
14
|
|
|
12
15
|
const router = Router();
|
|
13
16
|
|
|
14
17
|
const MAX_PREVIEW_BYTES = 1024 * 1024; // 1 MB — text content embedded in JSON
|
|
15
|
-
const MAX_RAW_BYTES = 50 * 1024 * 1024; // 50 MB — cap for binary
|
|
18
|
+
const MAX_RAW_BYTES = 50 * 1024 * 1024; // 50 MB — cap for non-media streaming (images/pdf/binary load whole into the browser)
|
|
19
|
+
// Audio/video are streamed via HTTP Range requests (see GET /raw),
|
|
20
|
+
// so the browser never buffers the whole file. Podcasts commonly
|
|
21
|
+
// run 100–300 MB and recorded video can run multi-GB; cap at 4 GB
|
|
22
|
+
// just to keep an obviously-pathological file from being served.
|
|
23
|
+
const MAX_MEDIA_BYTES = 4 * 1024 * 1024 * 1024;
|
|
16
24
|
const HIDDEN_DIRS = new Set([".git"]);
|
|
17
25
|
|
|
18
26
|
// Files whose basename exactly matches one of these is refused by
|
|
@@ -187,7 +195,7 @@ export function classify(filename: string): ContentKind {
|
|
|
187
195
|
// Cached realpath of the workspace. Computed once at module load so
|
|
188
196
|
// every request avoids the syscall. resolveWithinRoot needs an
|
|
189
197
|
// already-realpath'd root.
|
|
190
|
-
const workspaceReal =
|
|
198
|
+
const workspaceReal = realpathSync(workspacePath);
|
|
191
199
|
|
|
192
200
|
// Wraps the shared resolveWithinRoot helper with the additional
|
|
193
201
|
// hidden-dir traversal check (e.g. `.git/config`). `buildTreeAsync`
|
|
@@ -235,7 +243,7 @@ function resolveRefPath(prefixedPath: string): string | null {
|
|
|
235
243
|
|
|
236
244
|
let rootReal: string;
|
|
237
245
|
try {
|
|
238
|
-
rootReal =
|
|
246
|
+
rootReal = realpathSync(entry.hostPath);
|
|
239
247
|
} catch {
|
|
240
248
|
return null;
|
|
241
249
|
}
|
|
@@ -276,7 +284,7 @@ export function parseRange(header: string, size: number): ByteRange | null {
|
|
|
276
284
|
// RFC 7233 §2.1: "A Range request on a representation whose current
|
|
277
285
|
// length is 0 cannot be satisfied". We also need this guard at the
|
|
278
286
|
// top because the naive suffix-range math below produces `end = -1`
|
|
279
|
-
// for zero-byte files, which then crashes `
|
|
287
|
+
// for zero-byte files, which then crashes `createReadStream`
|
|
280
288
|
// with `ERR_OUT_OF_RANGE`.
|
|
281
289
|
if (size <= 0) return null;
|
|
282
290
|
const match = /^bytes=(\d*)-(\d*)$/i.exec(header.trim());
|
|
@@ -325,7 +333,7 @@ function applyRawSecurityHeaders(res: Response): void {
|
|
|
325
333
|
// If the read stream errors mid-flight (file deleted, disk error,
|
|
326
334
|
// permissions changed), surface a clean failure to the client instead
|
|
327
335
|
// of leaving the connection hanging.
|
|
328
|
-
function pipeWithErrorHandling(stream:
|
|
336
|
+
function pipeWithErrorHandling(stream: ReadStream, res: Response<ErrorResponse>): void {
|
|
329
337
|
stream.on("error", (err) => {
|
|
330
338
|
if (res.headersSent) {
|
|
331
339
|
res.destroy(err);
|
|
@@ -340,7 +348,7 @@ function pipeWithErrorHandling(stream: fs.ReadStream, res: Response<ErrorRespons
|
|
|
340
348
|
// the same security filters as the original sync implementation
|
|
341
349
|
// (hidden dirs, sensitive files, symlinks all rejected) and the same
|
|
342
350
|
// ordering (dirs before files, alphabetical within type). Uses
|
|
343
|
-
// `
|
|
351
|
+
// `promises` throughout so the walk never blocks the event loop,
|
|
344
352
|
// and fans out each directory's children in parallel via
|
|
345
353
|
// `Promise.all`.
|
|
346
354
|
//
|
|
@@ -475,6 +483,7 @@ export async function listDirShallow(absPath: string, relPath: string, gitFilter
|
|
|
475
483
|
}
|
|
476
484
|
|
|
477
485
|
router.get(API_ROUTES.files.tree, async (_req: Request<object, unknown, unknown, object>, res: Response<TreeNode | ErrorResponse>) => {
|
|
486
|
+
log.info("files", "GET tree: start");
|
|
478
487
|
try {
|
|
479
488
|
// Start with an empty filter — the workspace root's .gitignore
|
|
480
489
|
// is for git (excluding github/ from commits), NOT for the
|
|
@@ -485,7 +494,8 @@ router.get(API_ROUTES.files.tree, async (_req: Request<object, unknown, unknown,
|
|
|
485
494
|
const tree = await buildTreeAsync(workspaceReal, "");
|
|
486
495
|
res.json(tree);
|
|
487
496
|
} catch (err) {
|
|
488
|
-
|
|
497
|
+
log.error("files", "GET tree: threw", { error: errorMessage(err) });
|
|
498
|
+
serverError(res, `Failed to read workspace: ${errorMessage(err)}`);
|
|
489
499
|
}
|
|
490
500
|
});
|
|
491
501
|
|
|
@@ -493,17 +503,20 @@ router.get(API_ROUTES.files.tree, async (_req: Request<object, unknown, unknown,
|
|
|
493
503
|
// (no recursion) so the client can render the tree incrementally.
|
|
494
504
|
// `path` is optional; empty / missing = workspace root.
|
|
495
505
|
router.get(API_ROUTES.files.dir, async (req: Request<object, unknown, unknown, PathQuery>, res: Response<TreeNode | ErrorResponse>) => {
|
|
496
|
-
const relPath =
|
|
506
|
+
const relPath = getOptionalStringQuery(req, "path") ?? "";
|
|
507
|
+
log.info("files", "GET dir: start", { pathPreview: previewSnippet(relPath) });
|
|
497
508
|
|
|
498
509
|
// Reference directory branch — resolve against the registered ref dir
|
|
499
510
|
if (isRefPath(relPath)) {
|
|
500
511
|
const absPath = resolveRefPath(relPath);
|
|
501
512
|
if (!absPath) {
|
|
513
|
+
log.warn("files", "GET dir: ref dir not found", { pathPreview: previewSnippet(relPath) });
|
|
502
514
|
notFound(res, "Not found");
|
|
503
515
|
return;
|
|
504
516
|
}
|
|
505
517
|
const stat = await statSafeAsync(absPath);
|
|
506
518
|
if (!stat || !stat.isDirectory()) {
|
|
519
|
+
log.warn("files", "GET dir: ref path missing or not a dir", { pathPreview: previewSnippet(relPath) });
|
|
507
520
|
notFound(res, "Not found");
|
|
508
521
|
return;
|
|
509
522
|
}
|
|
@@ -515,15 +528,18 @@ router.get(API_ROUTES.files.dir, async (req: Request<object, unknown, unknown, P
|
|
|
515
528
|
// Workspace path — existing logic
|
|
516
529
|
const absPath = resolveSafe(relPath);
|
|
517
530
|
if (!absPath) {
|
|
531
|
+
log.warn("files", "GET dir: path outside workspace", { pathPreview: previewSnippet(relPath) });
|
|
518
532
|
notFound(res, "Not found");
|
|
519
533
|
return;
|
|
520
534
|
}
|
|
521
535
|
const stat = await statSafeAsync(absPath);
|
|
522
536
|
if (!stat) {
|
|
537
|
+
log.warn("files", "GET dir: not found", { pathPreview: previewSnippet(relPath) });
|
|
523
538
|
notFound(res, "Not found");
|
|
524
539
|
return;
|
|
525
540
|
}
|
|
526
541
|
if (!stat.isDirectory()) {
|
|
542
|
+
log.warn("files", "GET dir: not a directory", { pathPreview: previewSnippet(relPath) });
|
|
527
543
|
badRequest(res, "path is not a directory");
|
|
528
544
|
return;
|
|
529
545
|
}
|
|
@@ -541,6 +557,7 @@ router.get(API_ROUTES.files.dir, async (req: Request<object, unknown, unknown, P
|
|
|
541
557
|
const listing = await listDirShallow(absPath, path.relative(workspaceReal, absPath), filter);
|
|
542
558
|
res.json(listing);
|
|
543
559
|
} catch (err) {
|
|
560
|
+
log.error("files", "GET dir: threw", { pathPreview: previewSnippet(relPath), error: errorMessage(err) });
|
|
544
561
|
serverError(res, `Failed to read directory: ${errorMessage(err)}`);
|
|
545
562
|
}
|
|
546
563
|
});
|
|
@@ -567,8 +584,8 @@ interface PathQuery {
|
|
|
567
584
|
function resolveAndStatFile<T>(
|
|
568
585
|
req: Request<object, unknown, unknown, PathQuery>,
|
|
569
586
|
res: Response<T | ErrorResponse>,
|
|
570
|
-
): { relPath: string; absPath: string; stat:
|
|
571
|
-
const relPath =
|
|
587
|
+
): { relPath: string; absPath: string; stat: Stats } | null {
|
|
588
|
+
const relPath = getOptionalStringQuery(req, "path") ?? "";
|
|
572
589
|
if (!relPath) {
|
|
573
590
|
badRequest(res, "path required");
|
|
574
591
|
return null;
|
|
@@ -623,8 +640,16 @@ function resolveAndStatFile<T>(
|
|
|
623
640
|
}
|
|
624
641
|
|
|
625
642
|
router.get(API_ROUTES.files.content, (req: Request<object, unknown, unknown, PathQuery>, res: Response<FileContentResponse | ErrorResponse>) => {
|
|
643
|
+
const requestedPath = getOptionalStringQuery(req, "path") ?? "";
|
|
644
|
+
log.info("files", "GET content: start", { pathPreview: previewSnippet(requestedPath) });
|
|
626
645
|
const ctx = resolveAndStatFile(req, res);
|
|
627
|
-
if (!ctx)
|
|
646
|
+
if (!ctx) {
|
|
647
|
+
// resolveAndStatFile already wrote the 4xx; surface the gate
|
|
648
|
+
// miss so the operator can correlate the user-visible error
|
|
649
|
+
// with a concrete reason in the log without re-running.
|
|
650
|
+
log.warn("files", "GET content: gated by resolve/stat", { pathPreview: previewSnippet(requestedPath) });
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
628
653
|
const { relPath, absPath, stat } = ctx;
|
|
629
654
|
|
|
630
655
|
const meta = {
|
|
@@ -633,10 +658,13 @@ router.get(API_ROUTES.files.content, (req: Request<object, unknown, unknown, Pat
|
|
|
633
658
|
modifiedMs: stat.mtimeMs,
|
|
634
659
|
};
|
|
635
660
|
|
|
636
|
-
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
|
|
661
|
+
const kind = classify(absPath);
|
|
662
|
+
// Audio/video stream via Range requests, so they get the looser
|
|
663
|
+
// MAX_MEDIA_BYTES cap. Everything else (images/PDFs/binary) is
|
|
664
|
+
// loaded whole by the browser and stays at MAX_RAW_BYTES.
|
|
665
|
+
const isStreamingMedia = kind === "audio" || kind === "video";
|
|
666
|
+
const sizeCap = isStreamingMedia ? MAX_MEDIA_BYTES : MAX_RAW_BYTES;
|
|
667
|
+
if (stat.size > sizeCap) {
|
|
640
668
|
res.json({
|
|
641
669
|
kind: "too-large",
|
|
642
670
|
...meta,
|
|
@@ -645,7 +673,6 @@ router.get(API_ROUTES.files.content, (req: Request<object, unknown, unknown, Pat
|
|
|
645
673
|
return;
|
|
646
674
|
}
|
|
647
675
|
|
|
648
|
-
const kind = classify(absPath);
|
|
649
676
|
if (kind === "image" || kind === "pdf" || kind === "audio" || kind === "video") {
|
|
650
677
|
res.json({ kind, ...meta });
|
|
651
678
|
return;
|
|
@@ -668,11 +695,13 @@ router.get(API_ROUTES.files.content, (req: Request<object, unknown, unknown, Pat
|
|
|
668
695
|
}
|
|
669
696
|
let content: string;
|
|
670
697
|
try {
|
|
671
|
-
content =
|
|
698
|
+
content = readFileSync(absPath, "utf-8");
|
|
672
699
|
} catch (err) {
|
|
673
|
-
|
|
700
|
+
log.error("files", "GET content: read threw", { pathPreview: previewSnippet(relPath), error: errorMessage(err) });
|
|
701
|
+
serverError(res, `Failed to read file: ${errorMessage(err)}`);
|
|
674
702
|
return;
|
|
675
703
|
}
|
|
704
|
+
log.info("files", "GET content: ok", { pathPreview: previewSnippet(relPath), bytes: stat.size });
|
|
676
705
|
res.json({ kind: "text", ...meta, content });
|
|
677
706
|
});
|
|
678
707
|
|
|
@@ -682,15 +711,22 @@ router.get(API_ROUTES.files.content, (req: Request<object, unknown, unknown, Pat
|
|
|
682
711
|
// The file must already exist; creating new files is out of scope.
|
|
683
712
|
router.put(API_ROUTES.files.content, async (req: Request<object, unknown, WriteContentRequest>, res: Response<WriteContentResponse | ErrorResponse>) => {
|
|
684
713
|
const { path: relPathRaw, content: contentRaw } = req.body ?? {};
|
|
714
|
+
log.info("files", "PUT content: start", {
|
|
715
|
+
pathPreview: typeof relPathRaw === "string" ? previewSnippet(relPathRaw) : undefined,
|
|
716
|
+
bytes: typeof contentRaw === "string" ? Buffer.byteLength(contentRaw, "utf-8") : undefined,
|
|
717
|
+
});
|
|
685
718
|
if (typeof relPathRaw !== "string" || relPathRaw.length === 0) {
|
|
719
|
+
log.warn("files", "PUT content: missing path");
|
|
686
720
|
badRequest(res, "path required");
|
|
687
721
|
return;
|
|
688
722
|
}
|
|
689
723
|
if (typeof contentRaw !== "string") {
|
|
724
|
+
log.warn("files", "PUT content: missing content", { pathPreview: previewSnippet(relPathRaw) });
|
|
690
725
|
badRequest(res, "content required");
|
|
691
726
|
return;
|
|
692
727
|
}
|
|
693
728
|
if (Buffer.byteLength(contentRaw, "utf-8") > MAX_PREVIEW_BYTES) {
|
|
729
|
+
log.warn("files", "PUT content: too large", { pathPreview: previewSnippet(relPathRaw), bytes: Buffer.byteLength(contentRaw, "utf-8") });
|
|
694
730
|
badRequest(res, `content exceeds ${MAX_PREVIEW_BYTES} byte limit`);
|
|
695
731
|
return;
|
|
696
732
|
}
|
|
@@ -729,10 +765,15 @@ router.put(API_ROUTES.files.content, async (req: Request<object, unknown, WriteC
|
|
|
729
765
|
// other's staging file and race through the rename.
|
|
730
766
|
await writeFileAtomic(absPath, contentRaw, { uniqueTmp: true });
|
|
731
767
|
} catch (err) {
|
|
768
|
+
log.error("files", "PUT content: write threw", { pathPreview: previewSnippet(relPathRaw), error: errorMessage(err) });
|
|
732
769
|
serverError(res, `Failed to write file: ${errorMessage(err)}`);
|
|
733
770
|
return;
|
|
734
771
|
}
|
|
735
772
|
const fresh = await statSafeAsync(absPath);
|
|
773
|
+
log.info("files", "PUT content: ok", {
|
|
774
|
+
pathPreview: previewSnippet(relPathRaw),
|
|
775
|
+
bytes: fresh?.size ?? Buffer.byteLength(contentRaw, "utf-8"),
|
|
776
|
+
});
|
|
736
777
|
res.json({
|
|
737
778
|
path: relPathRaw,
|
|
738
779
|
size: fresh?.size ?? Buffer.byteLength(contentRaw, "utf-8"),
|
|
@@ -741,12 +782,20 @@ router.put(API_ROUTES.files.content, async (req: Request<object, unknown, WriteC
|
|
|
741
782
|
});
|
|
742
783
|
|
|
743
784
|
router.get(API_ROUTES.files.raw, (req: Request<object, unknown, unknown, PathQuery>, res: Response<ErrorResponse>) => {
|
|
785
|
+
const requestedPath = getOptionalStringQuery(req, "path") ?? "";
|
|
786
|
+
log.info("files", "GET raw: start", { pathPreview: previewSnippet(requestedPath) });
|
|
744
787
|
const ctx = resolveAndStatFile(req, res);
|
|
745
|
-
if (!ctx)
|
|
788
|
+
if (!ctx) {
|
|
789
|
+
log.warn("files", "GET raw: gated by resolve/stat", { pathPreview: previewSnippet(requestedPath) });
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
746
792
|
const { absPath, stat } = ctx;
|
|
747
793
|
|
|
748
|
-
|
|
749
|
-
|
|
794
|
+
const rawKind = classify(absPath);
|
|
795
|
+
const rawSizeCap = rawKind === "audio" || rawKind === "video" ? MAX_MEDIA_BYTES : MAX_RAW_BYTES;
|
|
796
|
+
if (stat.size > rawSizeCap) {
|
|
797
|
+
log.warn("files", "GET raw: too large", { pathPreview: previewSnippet(requestedPath), bytes: stat.size, cap: rawSizeCap });
|
|
798
|
+
sendError(res, 413, `File too large to stream (${stat.size} bytes, limit ${rawSizeCap})`);
|
|
750
799
|
return;
|
|
751
800
|
}
|
|
752
801
|
const ext = path.extname(absPath).toLowerCase();
|
|
@@ -779,12 +828,12 @@ router.get(API_ROUTES.files.raw, (req: Request<object, unknown, unknown, PathQue
|
|
|
779
828
|
res.status(206);
|
|
780
829
|
res.setHeader("Content-Range", `bytes ${range.start}-${range.end}/${stat.size}`);
|
|
781
830
|
res.setHeader("Content-Length", String(range.end - range.start + 1));
|
|
782
|
-
pipeWithErrorHandling(
|
|
831
|
+
pipeWithErrorHandling(createReadStream(absPath, { start: range.start, end: range.end }), res);
|
|
783
832
|
return;
|
|
784
833
|
}
|
|
785
834
|
|
|
786
835
|
res.setHeader("Content-Length", String(stat.size));
|
|
787
|
-
pipeWithErrorHandling(
|
|
836
|
+
pipeWithErrorHandling(createReadStream(absPath), res);
|
|
788
837
|
});
|
|
789
838
|
|
|
790
839
|
// ── Reference directory roots ───────────────────────────────────
|
|
@@ -794,6 +843,7 @@ router.get(API_ROUTES.files.raw, (req: Request<object, unknown, unknown, PathQue
|
|
|
794
843
|
// prefix so subsequent /dir and /content requests route correctly.
|
|
795
844
|
|
|
796
845
|
router.get(API_ROUTES.files.refRoots, async (_req: Request, res: Response<TreeNode[]>) => {
|
|
846
|
+
log.info("files", "GET ref-roots: start");
|
|
797
847
|
const entries = getCachedReferenceDirs();
|
|
798
848
|
const nodes: TreeNode[] = [];
|
|
799
849
|
for (const entry of entries) {
|
|
@@ -806,6 +856,7 @@ router.get(API_ROUTES.files.refRoots, async (_req: Request, res: Response<TreeNo
|
|
|
806
856
|
modifiedMs: stat.mtimeMs,
|
|
807
857
|
});
|
|
808
858
|
}
|
|
859
|
+
log.info("files", "GET ref-roots: ok", { configured: entries.length, mounted: nodes.length });
|
|
809
860
|
res.json(nodes);
|
|
810
861
|
});
|
|
811
862
|
|
|
@@ -3,12 +3,14 @@ import { readCurrentHtml, writeCurrentHtml } from "../../utils/files/html-io.js"
|
|
|
3
3
|
import { getGeminiClient, isGeminiAvailable } from "../../utils/gemini.js";
|
|
4
4
|
import { errorMessage } from "../../utils/errors.js";
|
|
5
5
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
6
|
+
import { log } from "../../system/logger/index.js";
|
|
7
|
+
import { promptMeta } from "../../utils/promptMeta.js";
|
|
6
8
|
|
|
7
9
|
const router = Router();
|
|
8
10
|
|
|
9
11
|
async function callGemini(prompt: string): Promise<string> {
|
|
10
|
-
const
|
|
11
|
-
const response = await
|
|
12
|
+
const client = getGeminiClient();
|
|
13
|
+
const response = await client.models.generateContent({
|
|
12
14
|
model: "gemini-2.0-flash",
|
|
13
15
|
contents: [{ text: prompt }],
|
|
14
16
|
});
|
|
@@ -41,11 +43,14 @@ type HtmlResponse = HtmlSuccessResponse | HtmlErrorResponse;
|
|
|
41
43
|
|
|
42
44
|
router.post(API_ROUTES.html.generate, async (req: Request<object, unknown, HtmlPromptBody>, res: Response<HtmlResponse>) => {
|
|
43
45
|
const { prompt } = req.body;
|
|
46
|
+
log.info("html", "generate: start", { prompt: typeof prompt === "string" ? promptMeta(prompt) : undefined });
|
|
44
47
|
if (!prompt) {
|
|
48
|
+
log.warn("html", "generate: missing prompt");
|
|
45
49
|
res.status(400).json({ message: "prompt is required" });
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
52
|
if (!isGeminiAvailable()) {
|
|
53
|
+
log.warn("html", "generate: GEMINI_API_KEY not set");
|
|
49
54
|
res.status(500).json({ message: "GEMINI_API_KEY is not set" });
|
|
50
55
|
return;
|
|
51
56
|
}
|
|
@@ -54,6 +59,7 @@ router.post(API_ROUTES.html.generate, async (req: Request<object, unknown, HtmlP
|
|
|
54
59
|
const html = await callGemini(fullPrompt);
|
|
55
60
|
|
|
56
61
|
await writeCurrentHtml(html);
|
|
62
|
+
log.info("html", "generate: ok", { bytes: html.length });
|
|
57
63
|
res.json({
|
|
58
64
|
message: "HTML generation succeeded",
|
|
59
65
|
instructions: "Acknowledge that the HTML was generated and has been presented to the user.",
|
|
@@ -61,23 +67,28 @@ router.post(API_ROUTES.html.generate, async (req: Request<object, unknown, HtmlP
|
|
|
61
67
|
data: { html, type: "tailwind" },
|
|
62
68
|
});
|
|
63
69
|
} catch (err) {
|
|
70
|
+
log.error("html", "generate: threw", { error: errorMessage(err), prompt: promptMeta(prompt) });
|
|
64
71
|
res.status(500).json({ message: errorMessage(err) });
|
|
65
72
|
}
|
|
66
73
|
});
|
|
67
74
|
|
|
68
75
|
router.post(API_ROUTES.html.edit, async (req: Request<object, unknown, HtmlPromptBody>, res: Response<HtmlResponse>) => {
|
|
69
76
|
const { prompt } = req.body;
|
|
77
|
+
log.info("html", "edit: start", { prompt: typeof prompt === "string" ? promptMeta(prompt) : undefined });
|
|
70
78
|
if (!prompt) {
|
|
79
|
+
log.warn("html", "edit: missing prompt");
|
|
71
80
|
res.status(400).json({ message: "prompt is required" });
|
|
72
81
|
return;
|
|
73
82
|
}
|
|
74
83
|
if (!isGeminiAvailable()) {
|
|
84
|
+
log.warn("html", "edit: GEMINI_API_KEY not set");
|
|
75
85
|
res.status(500).json({ message: "GEMINI_API_KEY is not set" });
|
|
76
86
|
return;
|
|
77
87
|
}
|
|
78
88
|
try {
|
|
79
89
|
const existingHtml = await readCurrentHtml();
|
|
80
90
|
if (!existingHtml?.trim()) {
|
|
91
|
+
log.warn("html", "edit: no existing HTML to modify");
|
|
81
92
|
res.status(400).json({
|
|
82
93
|
message: "No HTML page has been generated yet. Use generateHtml first.",
|
|
83
94
|
});
|
|
@@ -86,6 +97,7 @@ router.post(API_ROUTES.html.edit, async (req: Request<object, unknown, HtmlPromp
|
|
|
86
97
|
const fullPrompt = `Modify the following HTML page based on this instruction: ${prompt}\n\nExisting HTML:\n${existingHtml}\n\nRequirements:\n- Return only the complete modified HTML, no explanation`;
|
|
87
98
|
const html = await callGemini(fullPrompt);
|
|
88
99
|
await writeCurrentHtml(html);
|
|
100
|
+
log.info("html", "edit: ok", { bytes: html.length });
|
|
89
101
|
res.json({
|
|
90
102
|
message: "HTML editing succeeded",
|
|
91
103
|
instructions: "Acknowledge that the HTML was modified and has been presented to the user.",
|
|
@@ -94,6 +106,7 @@ router.post(API_ROUTES.html.edit, async (req: Request<object, unknown, HtmlPromp
|
|
|
94
106
|
updating: true,
|
|
95
107
|
});
|
|
96
108
|
} catch (err) {
|
|
109
|
+
log.error("html", "edit: threw", { error: errorMessage(err), prompt: promptMeta(prompt) });
|
|
97
110
|
res.status(500).json({ message: errorMessage(err) });
|
|
98
111
|
}
|
|
99
112
|
});
|
|
@@ -5,7 +5,15 @@ import { generateGeminiImageContent, generateGeminiImageFromPrompt } from "../..
|
|
|
5
5
|
import { errorMessage } from "../../utils/errors.js";
|
|
6
6
|
import { badRequest, serverError } from "../../utils/httpError.js";
|
|
7
7
|
import { saveImage, overwriteImage, loadImageBase64, stripDataUri, isImagePath } from "../../utils/files/image-store.js";
|
|
8
|
+
import { promptMeta } from "../../utils/promptMeta.js";
|
|
8
9
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
10
|
+
import { log } from "../../system/logger/index.js";
|
|
11
|
+
|
|
12
|
+
// Image-generation routes were silent on success and on failure. When
|
|
13
|
+
// the canvas showed "missing image" with no server-side trace, there
|
|
14
|
+
// was nothing to grep. Log lines now carry a prompt fingerprint
|
|
15
|
+
// (`{ length, sha256 }`) via `promptMeta()` instead of the raw text —
|
|
16
|
+
// see `server/utils/promptMeta.ts` for why.
|
|
9
17
|
|
|
10
18
|
const router = Router();
|
|
11
19
|
|
|
@@ -37,6 +45,17 @@ async function respondWithImage(
|
|
|
37
45
|
kind: "generation" | "edit",
|
|
38
46
|
): Promise<void> {
|
|
39
47
|
if (!imageData) {
|
|
48
|
+
// Gemini returned text-only / no image — typically a refusal,
|
|
49
|
+
// safety filter, or a quota miss. Codex flagged this branch
|
|
50
|
+
// (review of #780) for treating refusals as success; switching
|
|
51
|
+
// it to a 502 is the obvious fix, but `apiPost.extractError`
|
|
52
|
+
// only extracts `body.error` and image responses use
|
|
53
|
+
// `{ success: false, message }`, so the agent would lose the
|
|
54
|
+
// Gemini-side message and see only "Bad Gateway". Leaving
|
|
55
|
+
// behavior unchanged here until the shared error-shape
|
|
56
|
+
// (`extractError` accepting `message`, or all image responses
|
|
57
|
+
// adopting `error`) lands in a separate PR — see #783 review
|
|
58
|
+
// history.
|
|
40
59
|
res.json({ message: fallbackMessage ?? "no image data in response" });
|
|
41
60
|
return;
|
|
42
61
|
}
|
|
@@ -87,13 +106,27 @@ interface GenerateImageBody {
|
|
|
87
106
|
router.post(API_ROUTES.image.generate, async (req: Request<object, unknown, GenerateImageBody>, res: Response<ImageResponse>) => {
|
|
88
107
|
const { prompt, model } = req.body;
|
|
89
108
|
if (!prompt) {
|
|
109
|
+
log.warn("image", "generate: missing prompt");
|
|
90
110
|
res.status(400).json({ success: false, message: "prompt is required" });
|
|
91
111
|
return;
|
|
92
112
|
}
|
|
113
|
+
log.info("image", "generate: start", { prompt: promptMeta(prompt), model: model ?? "(default)" });
|
|
93
114
|
try {
|
|
94
115
|
const { imageData, message } = await generateGeminiImageFromPrompt(prompt, model);
|
|
116
|
+
if (!imageData) {
|
|
117
|
+
log.warn("image", "generate: gemini returned no image data", {
|
|
118
|
+
prompt: promptMeta(prompt),
|
|
119
|
+
fallbackMessage: message,
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
log.info("image", "generate: ok", { prompt: promptMeta(prompt), bytes: imageData.length });
|
|
123
|
+
}
|
|
95
124
|
await respondWithImage(res, imageData, message, prompt, "generation");
|
|
96
125
|
} catch (err) {
|
|
126
|
+
log.error("image", "generate: gemini call threw", {
|
|
127
|
+
prompt: promptMeta(prompt),
|
|
128
|
+
error: errorMessage(err),
|
|
129
|
+
});
|
|
97
130
|
res.status(500).json({ success: false, message: errorMessage(err) });
|
|
98
131
|
}
|
|
99
132
|
});
|
|
@@ -106,17 +139,20 @@ router.post(API_ROUTES.image.edit, async (req: Request<object, unknown, EditImag
|
|
|
106
139
|
const { prompt } = req.body;
|
|
107
140
|
const session = getOptionalStringQuery(req, "session");
|
|
108
141
|
if (!prompt) {
|
|
142
|
+
log.warn("image", "edit: missing prompt", { session });
|
|
109
143
|
res.status(400).json({ success: false, message: "prompt is required" });
|
|
110
144
|
return;
|
|
111
145
|
}
|
|
112
146
|
const currentImageData = session ? getSessionImageData(session) : undefined;
|
|
113
147
|
if (!currentImageData) {
|
|
148
|
+
log.warn("image", "edit: no source image selected", { session });
|
|
114
149
|
res.status(400).json({
|
|
115
150
|
success: false,
|
|
116
151
|
message: "No image is selected. Please click an image in the sidebar first, then ask me to edit it.",
|
|
117
152
|
});
|
|
118
153
|
return;
|
|
119
154
|
}
|
|
155
|
+
log.info("image", "edit: start", { prompt: promptMeta(prompt), session, sourceKind: isImagePath(currentImageData) ? "path" : "dataUri" });
|
|
120
156
|
try {
|
|
121
157
|
// Resolve input image to raw base64 — supports both file paths and legacy data URIs
|
|
122
158
|
const base64Data = isImagePath(currentImageData) ? await loadImageBase64(currentImageData) : stripDataUri(currentImageData);
|
|
@@ -127,8 +163,22 @@ router.post(API_ROUTES.image.edit, async (req: Request<object, unknown, EditImag
|
|
|
127
163
|
parts: [{ inlineData: { mimeType: "image/png", data: base64Data } }, { text: prompt }],
|
|
128
164
|
},
|
|
129
165
|
]);
|
|
166
|
+
if (!imageData) {
|
|
167
|
+
log.warn("image", "edit: gemini returned no image data", {
|
|
168
|
+
prompt: promptMeta(prompt),
|
|
169
|
+
session,
|
|
170
|
+
fallbackMessage: message,
|
|
171
|
+
});
|
|
172
|
+
} else {
|
|
173
|
+
log.info("image", "edit: ok", { prompt: promptMeta(prompt), session, bytes: imageData.length });
|
|
174
|
+
}
|
|
130
175
|
await respondWithImage(res, imageData, message, prompt, "edit");
|
|
131
176
|
} catch (err) {
|
|
177
|
+
log.error("image", "edit: gemini call threw", {
|
|
178
|
+
prompt: promptMeta(prompt),
|
|
179
|
+
session,
|
|
180
|
+
error: errorMessage(err),
|
|
181
|
+
});
|
|
132
182
|
res.status(500).json({ success: false, message: errorMessage(err) });
|
|
133
183
|
}
|
|
134
184
|
});
|
|
@@ -145,25 +195,30 @@ router.post(API_ROUTES.image.upload, async (req: Request<object, unknown, Canvas
|
|
|
145
195
|
await saveCanvasImage(res, base64, async (b64) => saveImage(b64));
|
|
146
196
|
});
|
|
147
197
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
);
|
|
198
|
+
interface UpdateImageBody extends CanvasImageBody {
|
|
199
|
+
relativePath: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Canvas saves come in with the workspace-relative path the file
|
|
203
|
+
// already lives at (returned at canvas creation), so the client never
|
|
204
|
+
// has to know how `saveImage` shards by YYYY/MM. The server validates
|
|
205
|
+
// the prefix + extension via `isImagePath`; `safeResolve` inside
|
|
206
|
+
// `overwriteImage` blocks any traversal.
|
|
207
|
+
router.put(API_ROUTES.image.update, async (req: Request<object, unknown, UpdateImageBody>, res: Response<CanvasImageResponse | CanvasImageError>) => {
|
|
208
|
+
const { relativePath, imageData } = req.body;
|
|
209
|
+
if (!relativePath || !isImagePath(relativePath)) {
|
|
210
|
+
badRequest(res, "invalid image relativePath");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!imageData) {
|
|
214
|
+
badRequest(res, "imageData is required");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const base64 = stripDataUri(imageData);
|
|
218
|
+
await saveCanvasImage(res, base64, async (b64) => {
|
|
219
|
+
await overwriteImage(relativePath, b64);
|
|
220
|
+
return relativePath;
|
|
221
|
+
});
|
|
222
|
+
});
|
|
168
223
|
|
|
169
224
|
export default router;
|