mulmoclaude 0.1.2 → 0.4.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-Cv5snK9D.js +5 -0
- package/client/assets/index-CubzmCVK.css +2 -0
- package/client/assets/{index-D8rhwXLq.js → index-DtcyExH9.js} +80 -61
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
- package/client/index.html +2 -4
- package/package.json +13 -13
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/config.ts +12 -12
- package/server/agent/index.ts +9 -3
- package/server/agent/mcp-server.ts +19 -19
- package/server/agent/mcp-tools/index.ts +6 -6
- package/server/agent/mcp-tools/x.ts +7 -6
- package/server/agent/prompt.ts +195 -29
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +10 -10
- package/server/agent/stream.ts +4 -4
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +21 -3
- package/server/api/routes/config.ts +1 -1
- package/server/api/routes/files.ts +22 -21
- package/server/api/routes/html.ts +2 -2
- package/server/api/routes/image.ts +7 -7
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +16 -6
- package/server/api/routes/roles.ts +2 -2
- package/server/api/routes/scheduler.ts +14 -12
- package/server/api/routes/schedulerHandlers.ts +12 -12
- package/server/api/routes/schedulerTasks.ts +19 -17
- package/server/api/routes/sessions.ts +26 -26
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +5 -5
- package/server/api/routes/sources.ts +3 -3
- package/server/api/routes/todosColumnsHandlers.ts +30 -30
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki.ts +36 -22
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +6 -6
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +17 -16
- package/server/events/scheduler-adapter.ts +20 -20
- package/server/events/session-store/index.ts +10 -10
- package/server/events/task-manager/index.ts +7 -7
- package/server/index.ts +59 -65
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +5 -5
- package/server/utils/date.ts +18 -18
- package/server/utils/files/atomic.ts +16 -16
- package/server/utils/files/html-io.ts +5 -5
- package/server/utils/files/image-store.ts +19 -8
- package/server/utils/files/journal-io.ts +4 -4
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-store.ts +4 -4
- package/server/utils/files/naming.ts +2 -2
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +12 -12
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-io.ts +5 -5
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/session-io.ts +35 -35
- package/server/utils/files/spreadsheet-store.ts +7 -7
- package/server/utils/files/todos-io.ts +9 -9
- package/server/utils/files/user-tasks-io.ts +5 -5
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +2 -2
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- 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/request.ts +12 -6
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/indexer.ts +15 -15
- package/server/workspace/chat-index/summarizer.ts +4 -4
- package/server/workspace/custom-dirs.ts +16 -16
- package/server/workspace/journal/archivist.ts +35 -35
- package/server/workspace/journal/dailyPass.ts +31 -28
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +4 -4
- package/server/workspace/journal/indexFile.ts +29 -25
- package/server/workspace/journal/optimizationPass.ts +2 -2
- package/server/workspace/journal/state.ts +6 -6
- package/server/workspace/paths.ts +3 -3
- package/server/workspace/reference-dirs.ts +20 -20
- package/server/workspace/roles.ts +6 -6
- package/server/workspace/skills/discovery.ts +4 -4
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +3 -3
- package/server/workspace/skills/user-tasks.ts +34 -34
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +10 -10
- package/server/workspace/sources/classifier.ts +7 -7
- package/server/workspace/sources/fetchers/arxiv.ts +7 -7
- package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
- package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +12 -12
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +36 -13
- package/server/workspace/sources/pipeline/index.ts +8 -13
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/plan.ts +15 -13
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/registry.ts +16 -16
- package/server/workspace/sources/robots.ts +14 -14
- package/server/workspace/sources/sourceState.ts +17 -10
- 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/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +26 -16
- package/server/workspace/wiki-backlinks/index.ts +8 -8
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +315 -141
- package/src/components/CanvasViewToggle.vue +10 -7
- package/src/components/ChatInput.vue +67 -33
- package/src/components/FileContentHeader.vue +7 -4
- package/src/components/FileContentRenderer.vue +20 -6
- package/src/components/FileTree.vue +6 -3
- package/src/components/FileTreePane.vue +11 -8
- package/src/components/FilesView.vue +5 -3
- package/src/components/LockStatusPopup.vue +17 -14
- package/src/components/NotificationBell.vue +14 -5
- package/src/components/NotificationToast.vue +6 -3
- package/src/components/PluginLauncher.vue +19 -56
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +2 -2
- package/src/components/SessionHistoryPanel.vue +38 -34
- package/src/components/SessionTabBar.vue +8 -10
- package/src/components/SettingsMcpTab.vue +49 -36
- package/src/components/SettingsModal.vue +24 -22
- package/src/components/SettingsReferenceDirsTab.vue +39 -34
- package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
- package/src/components/SidebarHeader.vue +25 -4
- package/src/components/StackView.vue +4 -1
- package/src/components/SuggestionsPanel.vue +7 -4
- package/src/components/TodoExplorer.vue +26 -15
- package/src/components/ToolResultsPanel.vue +27 -13
- package/src/components/todo/TodoAddDialog.vue +19 -14
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +17 -12
- package/src/components/todo/TodoKanbanView.vue +10 -5
- package/src/components/todo/TodoListView.vue +10 -7
- package/src/components/todo/TodoTableView.vue +5 -2
- package/src/composables/useAppApi.ts +9 -0
- package/src/composables/useClickOutside.ts +2 -2
- package/src/composables/useDynamicFavicon.ts +172 -37
- package/src/composables/useEventListeners.ts +7 -8
- package/src/composables/useFaviconState.ts +13 -2
- package/src/composables/useFileSelection.ts +24 -6
- package/src/composables/useFreshPluginData.ts +3 -3
- package/src/composables/useKeyNavigation.ts +11 -11
- package/src/composables/useLayoutMode.ts +32 -0
- package/src/composables/useMcpTools.ts +2 -2
- package/src/composables/useNotifications.ts +3 -3
- package/src/composables/usePdfDownload.ts +4 -4
- package/src/composables/usePendingCalls.ts +1 -1
- package/src/composables/usePubSub.ts +10 -10
- package/src/composables/useRoles.ts +1 -1
- package/src/composables/useSandboxStatus.ts +1 -1
- package/src/composables/useSessionDerived.ts +3 -3
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSessionSync.ts +8 -8
- package/src/composables/useViewLayout.ts +20 -34
- package/src/config/roles.ts +2 -2
- package/src/lang/de.ts +536 -0
- package/src/lang/en.ts +558 -0
- package/src/lang/es.ts +543 -0
- package/src/lang/fr.ts +536 -0
- package/src/lang/ja.ts +536 -0
- package/src/lang/ko.ts +540 -0
- package/src/lang/pt-BR.ts +534 -0
- package/src/lang/zh.ts +537 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +2 -0
- package/src/plugins/canvas/View.vue +102 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/chart/Preview.vue +5 -5
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +59 -43
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +29 -25
- package/src/plugins/manageSource/Preview.vue +2 -2
- package/src/plugins/manageSource/View.vue +73 -52
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +26 -36
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/helpers.ts +8 -8
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +40 -30
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/scheduler/Preview.vue +13 -10
- package/src/plugins/scheduler/TasksTab.vue +57 -28
- package/src/plugins/scheduler/View.vue +28 -19
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/spreadsheet/Preview.vue +8 -3
- package/src/plugins/spreadsheet/View.vue +21 -12
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +29 -9
- package/src/plugins/todo/Preview.vue +13 -8
- package/src/plugins/todo/View.vue +38 -24
- package/src/plugins/todo/composables/useTodos.ts +5 -5
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +10 -7
- package/src/plugins/wiki/View.vue +202 -81
- package/src/plugins/wiki/helpers.ts +4 -4
- package/src/plugins/wiki/route.ts +112 -0
- package/src/router/guards.ts +46 -28
- package/src/router/index.ts +41 -26
- package/src/types/session.ts +4 -3
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/request.ts +22 -3
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/dom/scrollable.ts +2 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/files/sortChildren.ts +6 -6
- package/src/utils/format/frontmatter.ts +6 -6
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
- package/src/utils/markdown/extractFirstH1.ts +2 -2
- package/src/utils/path/relativeLink.ts +15 -15
- package/src/utils/path/workspaceLinkRouter.ts +81 -0
- package/src/utils/role/icon.ts +2 -2
- package/src/utils/role/merge.ts +2 -2
- package/src/utils/role/plugins.ts +1 -1
- package/src/utils/session/sessionFactory.ts +2 -2
- package/src/utils/session/sessionHelpers.ts +2 -2
- package/src/utils/tools/dedup.ts +4 -4
- package/src/utils/tools/result.ts +3 -3
- package/src/utils/types.ts +2 -2
- 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-KNLBjwuh.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/utils/canvas/viewMode.ts +0 -46
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
package/server/utils/date.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* is a wall-clock question, not a UTC question.
|
|
9
9
|
*/
|
|
10
10
|
export function toLocalIsoDate(input: Date | number): string {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const day = String(
|
|
15
|
-
return `${
|
|
11
|
+
const dateValue = typeof input === "number" ? new Date(input) : input;
|
|
12
|
+
const year = dateValue.getFullYear();
|
|
13
|
+
const month = String(dateValue.getMonth() + 1).padStart(2, "0");
|
|
14
|
+
const day = String(dateValue.getDate()).padStart(2, "0");
|
|
15
|
+
return `${year}-${month}-${day}`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -20,11 +20,11 @@ export function toLocalIsoDate(input: Date | number): string {
|
|
|
20
20
|
* any context where the date must not shift with the server's
|
|
21
21
|
* local timezone.
|
|
22
22
|
*/
|
|
23
|
-
export function toUtcIsoDate(
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
return `${
|
|
23
|
+
export function toUtcIsoDate(timestamp: Date): string {
|
|
24
|
+
const year = timestamp.getUTCFullYear();
|
|
25
|
+
const month = String(timestamp.getUTCMonth() + 1).padStart(2, "0");
|
|
26
|
+
const day = String(timestamp.getUTCDate()).padStart(2, "0");
|
|
27
|
+
return `${year}-${month}-${day}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -41,16 +41,16 @@ export function isoDateOnly(iso: string): string {
|
|
|
41
41
|
* Does NOT validate month/day ranges (Feb 30 passes); that's the
|
|
42
42
|
* caller's or LLM's responsibility.
|
|
43
43
|
*/
|
|
44
|
-
export function isValidIsoDate(
|
|
45
|
-
if (
|
|
46
|
-
if (
|
|
47
|
-
return isNumeric(
|
|
44
|
+
export function isValidIsoDate(input: string): boolean {
|
|
45
|
+
if (input.length !== 10) return false;
|
|
46
|
+
if (input[4] !== "-" || input[7] !== "-") return false;
|
|
47
|
+
return isNumeric(input.slice(0, 4)) && isNumeric(input.slice(5, 7)) && isNumeric(input.slice(8, 10));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function isNumeric(
|
|
51
|
-
if (
|
|
52
|
-
for (let
|
|
53
|
-
const code =
|
|
50
|
+
function isNumeric(input: string): boolean {
|
|
51
|
+
if (input.length === 0) return false;
|
|
52
|
+
for (let index = 0; index < input.length; index++) {
|
|
53
|
+
const code = input.charCodeAt(index);
|
|
54
54
|
if (code < 48 || code > 57) return false;
|
|
55
55
|
}
|
|
56
56
|
return true;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Moved from server/utils/file.ts (issue #366 Phase 1). The old
|
|
7
7
|
// file re-exports these for backwards compat.
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import { mkdirSync, promises, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { randomUUID } from "crypto";
|
|
12
12
|
|
|
@@ -47,18 +47,18 @@ function isTransientRenameError(err: unknown): boolean {
|
|
|
47
47
|
return err.code === "EPERM" || err.code === "EBUSY" || err.code === "EACCES";
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async function renameWithWindowsRetry(
|
|
50
|
+
async function renameWithWindowsRetry(fromPath: string, toPath: string): Promise<void> {
|
|
51
51
|
for (const delayMs of RENAME_RETRY_DELAYS_MS) {
|
|
52
52
|
try {
|
|
53
|
-
await
|
|
53
|
+
await promises.rename(fromPath, toPath);
|
|
54
54
|
return;
|
|
55
55
|
} catch (err) {
|
|
56
56
|
if (!isTransientRenameError(err)) throw err;
|
|
57
|
-
await new Promise((
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
// Final attempt — let any error propagate.
|
|
61
|
-
await
|
|
61
|
+
await promises.rename(fromPath, toPath);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Sync sleep that parks the thread instead of burning CPU. Only
|
|
@@ -66,21 +66,21 @@ async function renameWithWindowsRetry(from: string, to: string): Promise<void> {
|
|
|
66
66
|
// case block is the sum of RENAME_RETRY_DELAYS_MS (~430ms) and only
|
|
67
67
|
// triggers under AV/indexer contention.
|
|
68
68
|
const SYNC_SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
69
|
-
function sleepSync(
|
|
70
|
-
Atomics.wait(SYNC_SLEEP_BUF, 0, 0,
|
|
69
|
+
function sleepSync(millis: number): void {
|
|
70
|
+
Atomics.wait(SYNC_SLEEP_BUF, 0, 0, millis);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function renameSyncWithWindowsRetry(
|
|
73
|
+
function renameSyncWithWindowsRetry(fromPath: string, toPath: string): void {
|
|
74
74
|
for (const delayMs of RENAME_RETRY_DELAYS_MS) {
|
|
75
75
|
try {
|
|
76
|
-
|
|
76
|
+
renameSync(fromPath, toPath);
|
|
77
77
|
return;
|
|
78
78
|
} catch (err) {
|
|
79
79
|
if (!isTransientRenameError(err)) throw err;
|
|
80
80
|
sleepSync(delayMs);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
renameSync(fromPath, toPath);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
@@ -90,15 +90,15 @@ function renameSyncWithWindowsRetry(from: string, to: string): void {
|
|
|
90
90
|
*/
|
|
91
91
|
export async function writeFileAtomic(filePath: string, content: string, opts: WriteAtomicOptions = {}): Promise<void> {
|
|
92
92
|
const tmp = opts.uniqueTmp ? `${filePath}.${randomUUID()}.tmp` : `${filePath}.tmp`;
|
|
93
|
-
await
|
|
93
|
+
await promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
94
94
|
try {
|
|
95
|
-
await
|
|
95
|
+
await promises.writeFile(tmp, content, {
|
|
96
96
|
encoding: "utf-8",
|
|
97
97
|
mode: opts.mode,
|
|
98
98
|
});
|
|
99
99
|
await renameWithWindowsRetry(tmp, filePath);
|
|
100
100
|
} catch (err) {
|
|
101
|
-
await
|
|
101
|
+
await promises.unlink(tmp).catch(() => {});
|
|
102
102
|
throw err;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
@@ -110,13 +110,13 @@ export async function writeFileAtomic(filePath: string, content: string, opts: W
|
|
|
110
110
|
*/
|
|
111
111
|
export function writeFileAtomicSync(filePath: string, content: string, opts: WriteAtomicOptions = {}): void {
|
|
112
112
|
const tmp = opts.uniqueTmp ? `${filePath}.${randomUUID()}.tmp` : `${filePath}.tmp`;
|
|
113
|
-
|
|
113
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
114
114
|
try {
|
|
115
|
-
|
|
115
|
+
writeFileSync(tmp, content, { encoding: "utf-8", mode: opts.mode });
|
|
116
116
|
renameSyncWithWindowsRetry(tmp, filePath);
|
|
117
117
|
} catch (err) {
|
|
118
118
|
try {
|
|
119
|
-
|
|
119
|
+
unlinkSync(tmp);
|
|
120
120
|
} catch {
|
|
121
121
|
// best-effort cleanup
|
|
122
122
|
}
|
|
@@ -9,12 +9,12 @@ import { workspacePath } from "../../workspace/paths.js";
|
|
|
9
9
|
import { readTextUnder, writeTextUnder } from "./workspace-io.js";
|
|
10
10
|
|
|
11
11
|
const HTML_REL = path.posix.join(WORKSPACE_DIRS.html, "current.html");
|
|
12
|
-
const root = (
|
|
12
|
+
const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
|
|
13
13
|
|
|
14
|
-
export async function readCurrentHtml(
|
|
15
|
-
return readTextUnder(root(
|
|
14
|
+
export async function readCurrentHtml(workspaceRoot?: string): Promise<string | null> {
|
|
15
|
+
return readTextUnder(root(workspaceRoot), HTML_REL);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export async function writeCurrentHtml(html: string,
|
|
19
|
-
await writeTextUnder(root(
|
|
18
|
+
export async function writeCurrentHtml(html: string, workspaceRoot?: string): Promise<void> {
|
|
19
|
+
await writeTextUnder(root(workspaceRoot), HTML_REL, html);
|
|
20
20
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { mkdir, readFile, realpath, writeFile } from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import crypto from "crypto";
|
|
4
4
|
import { WORKSPACE_DIRS, WORKSPACE_PATHS } from "../../workspace/paths.js";
|
|
@@ -12,8 +12,8 @@ let imagesDirReal: string | null = null;
|
|
|
12
12
|
|
|
13
13
|
async function ensureImagesDir(): Promise<string> {
|
|
14
14
|
if (imagesDirReal) return imagesDirReal;
|
|
15
|
-
await
|
|
16
|
-
imagesDirReal = await
|
|
15
|
+
await mkdir(IMAGES_DIR, { recursive: true });
|
|
16
|
+
imagesDirReal = await realpath(IMAGES_DIR);
|
|
17
17
|
return imagesDirReal;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -35,23 +35,23 @@ async function safeResolve(relativePath: string): Promise<string> {
|
|
|
35
35
|
/** Save raw base64 (no data URI prefix) as a PNG file. Returns the workspace-relative path. */
|
|
36
36
|
export async function saveImage(base64Data: string): Promise<string> {
|
|
37
37
|
await ensureImagesDir();
|
|
38
|
-
const
|
|
39
|
-
const filename = `${
|
|
38
|
+
const imageId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
39
|
+
const filename = `${imageId}.png`;
|
|
40
40
|
const absPath = path.join(IMAGES_DIR, filename);
|
|
41
|
-
await
|
|
41
|
+
await writeFile(absPath, Buffer.from(base64Data, "base64"));
|
|
42
42
|
return path.posix.join(WORKSPACE_DIRS.images, filename);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** Overwrite an existing image file. The relativePath must start with "images/". */
|
|
46
46
|
export async function overwriteImage(relativePath: string, base64Data: string): Promise<void> {
|
|
47
47
|
const absPath = await safeResolve(relativePath);
|
|
48
|
-
await
|
|
48
|
+
await writeFile(absPath, Buffer.from(base64Data, "base64"));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/** Read an image file and return raw base64 (no data URI prefix). */
|
|
52
52
|
export async function loadImageBase64(relativePath: string): Promise<string> {
|
|
53
53
|
const absPath = await safeResolve(relativePath);
|
|
54
|
-
const buf = await
|
|
54
|
+
const buf = await readFile(absPath);
|
|
55
55
|
return buf.toString("base64");
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -64,3 +64,14 @@ export function stripDataUri(dataUri: string): string {
|
|
|
64
64
|
export function isImagePath(value: string): boolean {
|
|
65
65
|
return value.startsWith(`${WORKSPACE_DIRS.images}/`) && value.endsWith(".png");
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/** Build the workspace-relative image path for a filename, or null
|
|
69
|
+
* if the derived path wouldn't satisfy `isImagePath` (e.g. empty
|
|
70
|
+
* name, wrong extension). Keeps route handlers from reaching for
|
|
71
|
+
* `WORKSPACE_DIRS.images` directly — the images/ convention lives
|
|
72
|
+
* in exactly one place. */
|
|
73
|
+
export function imagePathFromFilename(filename: string): string | null {
|
|
74
|
+
if (!filename) return null;
|
|
75
|
+
const relativePath = path.posix.join(WORKSPACE_DIRS.images, filename);
|
|
76
|
+
return isImagePath(relativePath) ? relativePath : null;
|
|
77
|
+
}
|
|
@@ -17,7 +17,7 @@ import { isEnoent } from "./safe.js";
|
|
|
17
17
|
import { log } from "../../system/logger/index.js";
|
|
18
18
|
import { summariesRoot, dailyPathFor, topicPathFor, TOPICS_DIR, INDEX_FILE, STATE_FILE, DAILY_DIR, ARCHIVE_DIR } from "../../workspace/journal/paths.js";
|
|
19
19
|
|
|
20
|
-
import
|
|
20
|
+
import { statSync } from "node:fs";
|
|
21
21
|
|
|
22
22
|
const root = (rootOverride?: string) => rootOverride ?? workspacePath;
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ const root = (rootOverride?: string) => rootOverride ?? workspacePath;
|
|
|
26
26
|
export function journalStateExists(rootOverride?: string): boolean {
|
|
27
27
|
const filePath = path.join(summariesRoot(root(rootOverride)), STATE_FILE);
|
|
28
28
|
try {
|
|
29
|
-
|
|
29
|
+
statSync(filePath);
|
|
30
30
|
return true;
|
|
31
31
|
} catch {
|
|
32
32
|
return false;
|
|
@@ -51,9 +51,9 @@ export async function writeJournalState(state: unknown, rootOverride?: string):
|
|
|
51
51
|
|
|
52
52
|
// ── Index ───────────────────────────────────────────────────────
|
|
53
53
|
|
|
54
|
-
export async function writeJournalIndex(
|
|
54
|
+
export async function writeJournalIndex(markdown: string, rootOverride?: string): Promise<void> {
|
|
55
55
|
const filePath = path.join(summariesRoot(root(rootOverride)), INDEX_FILE);
|
|
56
|
-
await writeFileAtomic(filePath,
|
|
56
|
+
await writeFileAtomic(filePath, markdown);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// ── Daily summaries ─────────────────────────────────────────────
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Moved from server/utils/file.ts (issue #366 Phase 1). The old
|
|
4
4
|
// file re-exports these for backwards compat.
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { mkdirSync, promises, readFileSync, writeFileSync } from "fs";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import { writeFileAtomic } from "./atomic.js";
|
|
9
9
|
import { isEnoent } from "./safe.js";
|
|
@@ -20,7 +20,7 @@ import { log } from "../../system/logger/index.js";
|
|
|
20
20
|
export function loadJsonFile<T>(filePath: string, defaultValue: T): T {
|
|
21
21
|
let raw: string;
|
|
22
22
|
try {
|
|
23
|
-
raw =
|
|
23
|
+
raw = readFileSync(filePath, "utf-8");
|
|
24
24
|
} catch (err) {
|
|
25
25
|
if (isEnoent(err)) return defaultValue;
|
|
26
26
|
log.error("json", "loadJsonFile read failed", {
|
|
@@ -41,8 +41,8 @@ export function loadJsonFile<T>(filePath: string, defaultValue: T): T {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function saveJsonFile(filePath: string, data: unknown): void {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
45
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// ── Async ───────────────────────────────────────────────────────
|
|
@@ -60,7 +60,7 @@ export async function writeJsonAtomic(filePath: string, data: unknown, opts: Par
|
|
|
60
60
|
*/
|
|
61
61
|
export async function readJsonOrNull<T>(filePath: string): Promise<T | null> {
|
|
62
62
|
try {
|
|
63
|
-
const content = await
|
|
63
|
+
const content = await promises.readFile(filePath, "utf-8");
|
|
64
64
|
const parsed: T = JSON.parse(content);
|
|
65
65
|
return parsed;
|
|
66
66
|
} catch {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { workspacePath } from "../../workspace/workspace.js";
|
|
4
4
|
import { WORKSPACE_DIRS } from "../../workspace/paths.js";
|
|
@@ -11,20 +11,20 @@ import { buildArtifactPathRandom } from "./naming.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export async function saveMarkdown(content: string, prefix: string): Promise<string> {
|
|
13
13
|
const relPath = buildArtifactPathRandom(WORKSPACE_DIRS.markdowns, prefix, ".md", "document");
|
|
14
|
-
await
|
|
14
|
+
await writeFile(path.join(workspacePath, relPath), content, "utf-8");
|
|
15
15
|
return relPath;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Read a markdown file and return its content. */
|
|
19
19
|
export async function loadMarkdown(relativePath: string): Promise<string> {
|
|
20
20
|
const absPath = path.join(workspacePath, relativePath);
|
|
21
|
-
return
|
|
21
|
+
return readFile(absPath, "utf-8");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Overwrite an existing markdown file. */
|
|
25
25
|
export async function overwriteMarkdown(relativePath: string, content: string): Promise<void> {
|
|
26
26
|
const absPath = path.join(workspacePath, relativePath);
|
|
27
|
-
await
|
|
27
|
+
await writeFile(absPath, content, "utf-8");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** Check if a string is a markdown file path (not inline content). */
|
|
@@ -44,7 +44,7 @@ export function buildArtifactPathRandom(dir: string, prefix: string, ext: string
|
|
|
44
44
|
// Pass fallbackSlug as slugify's default so it overrides slugify's
|
|
45
45
|
// built-in "page" default when `prefix` sanitizes to empty.
|
|
46
46
|
const slug = slugify(prefix, fallbackSlug);
|
|
47
|
-
const
|
|
48
|
-
const fname = `${slug}-${
|
|
47
|
+
const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, RANDOM_SUFFIX_LEN);
|
|
48
|
+
const fname = `${slug}-${suffix}${ext}`;
|
|
49
49
|
return path.posix.join(dir, fname);
|
|
50
50
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// All fs access is funneled through shared helpers so path changes
|
|
5
5
|
// propagate from a single constant.
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { mkdirSync, statSync } from "fs";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { WORKSPACE_DIRS, workspacePath } from "../../workspace/paths.js";
|
|
10
10
|
import { loadJsonFile } from "./json.js";
|
|
@@ -31,14 +31,14 @@ export function readReferenceDirsJson(root?: string): unknown[] {
|
|
|
31
31
|
/** Write reference-dirs.json atomically. Creates config/ if needed. */
|
|
32
32
|
export function writeReferenceDirsJson(entries: readonly unknown[], root?: string): void {
|
|
33
33
|
const filePath = configPath(root ?? workspacePath);
|
|
34
|
-
|
|
34
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
35
35
|
writeFileAtomicSync(filePath, JSON.stringify(entries, null, 2));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** Check whether a host path exists and is a directory. */
|
|
39
39
|
export function isExistingDirectory(hostPath: string): boolean {
|
|
40
40
|
try {
|
|
41
|
-
return
|
|
41
|
+
return statSync(hostPath).isDirectory();
|
|
42
42
|
} catch {
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
// Optional `root` for test DI.
|
|
5
5
|
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import
|
|
7
|
+
import { mkdirSync, statSync, unlinkSync } from "node:fs";
|
|
8
8
|
import { WORKSPACE_DIRS } from "../../workspace/paths.js";
|
|
9
9
|
import { workspacePath } from "../../workspace/paths.js";
|
|
10
10
|
import { writeFileAtomicSync } from "./atomic.js";
|
|
11
11
|
import { isEnoent } from "./safe.js";
|
|
12
12
|
|
|
13
|
-
const root = (
|
|
13
|
+
const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
|
|
14
14
|
|
|
15
|
-
function roleFilePath(
|
|
16
|
-
return path.join(root(
|
|
15
|
+
function roleFilePath(roleId: string, workspaceRoot?: string): string {
|
|
16
|
+
return path.join(root(workspaceRoot), WORKSPACE_DIRS.roles, `${roleId}.json`);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Check if a custom role file exists. */
|
|
20
|
-
export function roleExists(
|
|
20
|
+
export function roleExists(roleId: string, workspaceRoot?: string): boolean {
|
|
21
21
|
try {
|
|
22
|
-
|
|
22
|
+
statSync(roleFilePath(roleId, workspaceRoot));
|
|
23
23
|
return true;
|
|
24
24
|
} catch {
|
|
25
25
|
return false;
|
|
@@ -27,9 +27,9 @@ export function roleExists(id: string, r?: string): boolean {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Delete a custom role file. Returns false if not found. */
|
|
30
|
-
export function deleteRole(
|
|
30
|
+
export function deleteRole(roleId: string, workspaceRoot?: string): boolean {
|
|
31
31
|
try {
|
|
32
|
-
|
|
32
|
+
unlinkSync(roleFilePath(roleId, workspaceRoot));
|
|
33
33
|
return true;
|
|
34
34
|
} catch (err) {
|
|
35
35
|
if (isEnoent(err)) return false;
|
|
@@ -38,8 +38,8 @@ export function deleteRole(id: string, r?: string): boolean {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** Save (create or overwrite) a custom role file atomically. */
|
|
41
|
-
export function saveRole(
|
|
42
|
-
const dir = path.join(root(
|
|
43
|
-
|
|
44
|
-
writeFileAtomicSync(roleFilePath(
|
|
41
|
+
export function saveRole(roleId: string, data: unknown, workspaceRoot?: string): void {
|
|
42
|
+
const dir = path.join(root(workspaceRoot), WORKSPACE_DIRS.roles);
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
writeFileAtomicSync(roleFilePath(roleId, workspaceRoot), JSON.stringify(data, null, 2));
|
|
45
45
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Moved from server/utils/fs.ts (issue #366 Phase 1). The old
|
|
8
8
|
// file re-exports these for backwards compat.
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import { Dirent, Stats, promises, readFileSync, readdirSync, realpathSync, statSync } from "fs";
|
|
11
11
|
import path from "path";
|
|
12
12
|
import { isErrorWithCode } from "../types.js";
|
|
13
13
|
|
|
@@ -19,7 +19,7 @@ export function isEnoent(err: unknown): boolean {
|
|
|
19
19
|
/** Read a binary file by absolute path. Null on ENOENT. */
|
|
20
20
|
export function readBinarySafeSync(absPath: string): Buffer | null {
|
|
21
21
|
try {
|
|
22
|
-
return
|
|
22
|
+
return readFileSync(absPath);
|
|
23
23
|
} catch {
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
@@ -28,7 +28,7 @@ export function readBinarySafeSync(absPath: string): Buffer | null {
|
|
|
28
28
|
/** Read a text file by absolute path (async). Null on ENOENT. */
|
|
29
29
|
export async function readTextSafe(absPath: string): Promise<string | null> {
|
|
30
30
|
try {
|
|
31
|
-
return await
|
|
31
|
+
return await promises.readFile(absPath, "utf-8");
|
|
32
32
|
} catch {
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
@@ -37,39 +37,39 @@ export async function readTextSafe(absPath: string): Promise<string | null> {
|
|
|
37
37
|
/** Read a text file by absolute path (sync). Null on ENOENT. */
|
|
38
38
|
export function readTextSafeSync(absPath: string): string | null {
|
|
39
39
|
try {
|
|
40
|
-
return
|
|
40
|
+
return readFileSync(absPath, "utf-8");
|
|
41
41
|
} catch {
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function statSafe(absPath: string):
|
|
46
|
+
export function statSafe(absPath: string): Stats | null {
|
|
47
47
|
try {
|
|
48
|
-
return
|
|
48
|
+
return statSync(absPath);
|
|
49
49
|
} catch {
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export async function statSafeAsync(absPath: string): Promise<
|
|
54
|
+
export async function statSafeAsync(absPath: string): Promise<Stats | null> {
|
|
55
55
|
try {
|
|
56
|
-
return await
|
|
56
|
+
return await promises.stat(absPath);
|
|
57
57
|
} catch {
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export function readDirSafe(absPath: string):
|
|
62
|
+
export function readDirSafe(absPath: string): Dirent[] {
|
|
63
63
|
try {
|
|
64
|
-
return
|
|
64
|
+
return readdirSync(absPath, { withFileTypes: true });
|
|
65
65
|
} catch {
|
|
66
66
|
return [];
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export async function readDirSafeAsync(absPath: string): Promise<
|
|
70
|
+
export async function readDirSafeAsync(absPath: string): Promise<Dirent[]> {
|
|
71
71
|
try {
|
|
72
|
-
return await
|
|
72
|
+
return await promises.readdir(absPath, { withFileTypes: true });
|
|
73
73
|
} catch {
|
|
74
74
|
return [];
|
|
75
75
|
}
|
|
@@ -77,7 +77,7 @@ export async function readDirSafeAsync(absPath: string): Promise<fs.Dirent[]> {
|
|
|
77
77
|
|
|
78
78
|
export async function readTextOrNull(file: string): Promise<string | null> {
|
|
79
79
|
try {
|
|
80
|
-
return await
|
|
80
|
+
return await promises.readFile(file, "utf-8");
|
|
81
81
|
} catch {
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
@@ -95,7 +95,7 @@ export function resolveWithinRoot(rootReal: string, relPath: string): string | n
|
|
|
95
95
|
const resolved = path.resolve(rootReal, normalized);
|
|
96
96
|
let resolvedReal: string;
|
|
97
97
|
try {
|
|
98
|
-
resolvedReal =
|
|
98
|
+
resolvedReal = realpathSync(resolved);
|
|
99
99
|
} catch {
|
|
100
100
|
return null;
|
|
101
101
|
}
|
|
@@ -9,12 +9,12 @@ import { resolvePath } from "./workspace-io.js";
|
|
|
9
9
|
import { loadJsonFile } from "./json.js";
|
|
10
10
|
import { writeFileAtomicSync } from "./atomic.js";
|
|
11
11
|
|
|
12
|
-
const root = (
|
|
12
|
+
const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
|
|
13
13
|
|
|
14
|
-
export function loadSchedulerItems<T>(fallback: T,
|
|
15
|
-
return loadJsonFile(resolvePath(root(
|
|
14
|
+
export function loadSchedulerItems<T>(fallback: T, workspaceRoot?: string): T {
|
|
15
|
+
return loadJsonFile(resolvePath(root(workspaceRoot), WORKSPACE_FILES.schedulerItems), fallback);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function saveSchedulerItems(items: unknown,
|
|
19
|
-
writeFileAtomicSync(resolvePath(root(
|
|
18
|
+
export function saveSchedulerItems(items: unknown, workspaceRoot?: string): void {
|
|
19
|
+
writeFileAtomicSync(resolvePath(root(workspaceRoot), WORKSPACE_FILES.schedulerItems), JSON.stringify(items, null, 2));
|
|
20
20
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// system task ID (e.g. "system:journal"), value overrides the
|
|
5
5
|
// default schedule.
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { mkdirSync } from "fs";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { workspacePath } from "../../workspace/paths.js";
|
|
10
10
|
import { WORKSPACE_FILES } from "../../../src/config/workspacePaths.js";
|
|
@@ -59,6 +59,6 @@ export function loadSchedulerOverrides(root?: string): ScheduleOverrides {
|
|
|
59
59
|
/** Save schedule overrides atomically. Creates directory if needed. */
|
|
60
60
|
export function saveSchedulerOverrides(overrides: ScheduleOverrides, root?: string): void {
|
|
61
61
|
const filePath = overridesPath(root);
|
|
62
|
-
|
|
62
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
63
63
|
writeFileAtomicSync(filePath, JSON.stringify(overrides, null, 2));
|
|
64
64
|
}
|