mulmoclaude 0.3.0 → 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-eHWB79u5.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/index.ts +9 -3
- package/server/agent/mcp-tools/index.ts +6 -6
- package/server/agent/mcp-tools/x.ts +2 -1
- 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 +21 -3
- package/server/api/routes/config.ts +1 -1
- package/server/api/routes/files.ts +13 -12
- 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 +8 -6
- package/server/api/routes/schedulerTasks.ts +5 -3
- package/server/api/routes/sessions.ts +2 -2
- 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/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki.ts +22 -8
- 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/index.ts +40 -46
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +5 -5
- package/server/utils/files/atomic.ts +11 -11
- package/server/utils/files/image-store.ts +17 -6
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-store.ts +4 -4
- 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 +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/summarizer.ts +4 -4
- package/server/workspace/custom-dirs.ts +5 -5
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +4 -4
- 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 +3 -3
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +3 -3
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +2 -2
- 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 +36 -13
- package/server/workspace/sources/pipeline/index.ts +2 -7
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/plan.ts +11 -9
- 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 +286 -112
- package/src/components/CanvasViewToggle.vue +10 -7
- package/src/components/ChatInput.vue +60 -26
- 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 +15 -12
- package/src/components/NotificationBell.vue +14 -5
- package/src/components/NotificationToast.vue +4 -1
- package/src/components/PluginLauncher.vue +19 -56
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/SessionHistoryPanel.vue +33 -29
- package/src/components/SessionTabBar.vue +8 -10
- package/src/components/SettingsMcpTab.vue +43 -30
- package/src/components/SettingsModal.vue +21 -19
- package/src/components/SettingsReferenceDirsTab.vue +29 -24
- package/src/components/SettingsWorkspaceDirsTab.vue +32 -22
- package/src/components/SidebarHeader.vue +25 -4
- package/src/components/StackView.vue +4 -1
- package/src/components/SuggestionsPanel.vue +5 -2
- package/src/components/TodoExplorer.vue +26 -15
- package/src/components/ToolResultsPanel.vue +27 -13
- 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 +10 -5
- package/src/components/todo/TodoListView.vue +5 -2
- package/src/components/todo/TodoTableView.vue +5 -2
- package/src/composables/useAppApi.ts +9 -0
- 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/useLayoutMode.ts +32 -0
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useViewLayout.ts +20 -34
- 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 +1 -1
- 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 +26 -22
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/manageSource/View.vue +73 -52
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +24 -34
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +36 -26
- package/src/plugins/scheduler/Preview.vue +7 -4
- package/src/plugins/scheduler/TasksTab.vue +53 -24
- 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 +27 -7
- package/src/plugins/todo/Preview.vue +11 -6
- package/src/plugins/todo/View.vue +27 -13
- 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 +202 -81
- package/src/plugins/wiki/route.ts +112 -0
- package/src/router/guards.ts +42 -24
- package/src/router/index.ts +41 -26
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/request.ts +19 -0
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/path/workspaceLinkRouter.ts +81 -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/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
|
@@ -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
|
|
|
@@ -38,20 +38,20 @@ export async function saveImage(base64Data: string): Promise<string> {
|
|
|
38
38
|
const imageId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
39
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;
|
|
@@ -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). */
|
|
@@ -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,7 +4,7 @@
|
|
|
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";
|
|
@@ -19,7 +19,7 @@ function roleFilePath(roleId: string, workspaceRoot?: string): string {
|
|
|
19
19
|
/** Check if a custom role file exists. */
|
|
20
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;
|
|
@@ -29,7 +29,7 @@ export function roleExists(roleId: string, workspaceRoot?: string): boolean {
|
|
|
29
29
|
/** Delete a custom role file. Returns false if not found. */
|
|
30
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;
|
|
@@ -40,6 +40,6 @@ export function deleteRole(roleId: string, workspaceRoot?: string): boolean {
|
|
|
40
40
|
/** Save (create or overwrite) a custom role file atomically. */
|
|
41
41
|
export function saveRole(roleId: string, data: unknown, workspaceRoot?: string): void {
|
|
42
42
|
const dir = path.join(root(workspaceRoot), WORKSPACE_DIRS.roles);
|
|
43
|
-
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
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
|
}
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { mkdir, 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";
|
|
@@ -13,8 +13,8 @@ let spreadsheetsDirReal: string | null = null;
|
|
|
13
13
|
|
|
14
14
|
async function ensureSpreadsheetsDir(): Promise<string> {
|
|
15
15
|
if (spreadsheetsDirReal) return spreadsheetsDirReal;
|
|
16
|
-
await
|
|
17
|
-
spreadsheetsDirReal = await
|
|
16
|
+
await mkdir(SPREADSHEETS_DIR, { recursive: true });
|
|
17
|
+
spreadsheetsDirReal = await realpath(SPREADSHEETS_DIR);
|
|
18
18
|
return spreadsheetsDirReal;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -38,14 +38,14 @@ export async function saveSpreadsheet(sheets: unknown[]): Promise<string> {
|
|
|
38
38
|
await ensureSpreadsheetsDir();
|
|
39
39
|
const sheetId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
40
40
|
const filename = `${sheetId}.json`;
|
|
41
|
-
await
|
|
41
|
+
await writeFile(path.join(SPREADSHEETS_DIR, filename), JSON.stringify(sheets), "utf-8");
|
|
42
42
|
return path.posix.join(WORKSPACE_DIRS.spreadsheets, filename);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** Overwrite an existing spreadsheet file. */
|
|
46
46
|
export async function overwriteSpreadsheet(relativePath: string, sheets: unknown[]): Promise<void> {
|
|
47
47
|
const absPath = await safeResolve(relativePath);
|
|
48
|
-
await
|
|
48
|
+
await writeFile(absPath, JSON.stringify(sheets), "utf-8");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/** Check if a string is a spreadsheet file path (not inline data).
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// All reads swallow ENOENT and return null / fallback so callers can
|
|
12
12
|
// do `if (!content)` instead of try/catch.
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import { Stats, mkdirSync, promises, readFileSync, readdirSync, statSync } from "fs";
|
|
15
15
|
import path from "path";
|
|
16
16
|
import { workspacePath } from "../../workspace/paths.js";
|
|
17
17
|
import { writeFileAtomic, writeFileAtomicSync } from "./atomic.js";
|
|
@@ -44,7 +44,7 @@ export function resolveWorkspacePath(relPath: string): string {
|
|
|
44
44
|
*/
|
|
45
45
|
export async function readWorkspaceText(relPath: string): Promise<string | null> {
|
|
46
46
|
try {
|
|
47
|
-
return await
|
|
47
|
+
return await promises.readFile(resolveWorkspacePath(relPath), "utf-8");
|
|
48
48
|
} catch (err) {
|
|
49
49
|
return rethrowUnexpected(err, `readWorkspaceText(${relPath})`);
|
|
50
50
|
}
|
|
@@ -53,7 +53,7 @@ export async function readWorkspaceText(relPath: string): Promise<string | null>
|
|
|
53
53
|
/** Sync variant. Same ENOENT-only swallow contract. */
|
|
54
54
|
export function readWorkspaceTextSync(relPath: string): string | null {
|
|
55
55
|
try {
|
|
56
|
-
return
|
|
56
|
+
return readFileSync(resolveWorkspacePath(relPath), "utf-8");
|
|
57
57
|
} catch (err) {
|
|
58
58
|
return rethrowUnexpected(err, `readWorkspaceTextSync(${relPath})`);
|
|
59
59
|
}
|
|
@@ -133,7 +133,7 @@ export function resolvePath(root: string, relPath: string): string {
|
|
|
133
133
|
* unexpected errors. */
|
|
134
134
|
export async function readTextUnder(root: string, relPath: string): Promise<string | null> {
|
|
135
135
|
try {
|
|
136
|
-
return await
|
|
136
|
+
return await promises.readFile(path.join(root, relPath), "utf-8");
|
|
137
137
|
} catch (err) {
|
|
138
138
|
return rethrowUnexpected(err, `readTextUnder(${relPath})`);
|
|
139
139
|
}
|
|
@@ -147,7 +147,7 @@ export async function writeTextUnder(root: string, relPath: string, content: str
|
|
|
147
147
|
/** Sync read text under a root. Null on ENOENT. */
|
|
148
148
|
export function readTextUnderSync(root: string, relPath: string): string | null {
|
|
149
149
|
try {
|
|
150
|
-
return
|
|
150
|
+
return readFileSync(path.join(root, relPath), "utf-8");
|
|
151
151
|
} catch (err) {
|
|
152
152
|
return rethrowUnexpected(err, `readTextUnderSync(${relPath})`);
|
|
153
153
|
}
|
|
@@ -156,7 +156,7 @@ export function readTextUnderSync(root: string, relPath: string): string | null
|
|
|
156
156
|
/** Sync readdir under a root. Empty on ENOENT. */
|
|
157
157
|
export function readdirUnderSync(root: string, relPath: string): string[] {
|
|
158
158
|
try {
|
|
159
|
-
return
|
|
159
|
+
return readdirSync(path.join(root, relPath));
|
|
160
160
|
} catch (err) {
|
|
161
161
|
if (isEnoent(err)) return [];
|
|
162
162
|
log.error("workspace-io", `readdirUnderSync(${relPath})`, {
|
|
@@ -169,7 +169,7 @@ export function readdirUnderSync(root: string, relPath: string): string[] {
|
|
|
169
169
|
/** Readdir under a root. Empty on ENOENT; rethrows unexpected. */
|
|
170
170
|
export async function readdirUnder(root: string, relPath: string): Promise<string[]> {
|
|
171
171
|
try {
|
|
172
|
-
return await
|
|
172
|
+
return await promises.readdir(path.join(root, relPath));
|
|
173
173
|
} catch (err) {
|
|
174
174
|
if (isEnoent(err)) return [];
|
|
175
175
|
log.error("workspace-io", `readdirUnder(${relPath})`, {
|
|
@@ -180,9 +180,9 @@ export async function readdirUnder(root: string, relPath: string): Promise<strin
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/** Stat under a root. Null on ENOENT; rethrows unexpected. */
|
|
183
|
-
export async function statUnder(root: string, relPath: string): Promise<
|
|
183
|
+
export async function statUnder(root: string, relPath: string): Promise<Stats | null> {
|
|
184
184
|
try {
|
|
185
|
-
return await
|
|
185
|
+
return await promises.stat(path.join(root, relPath));
|
|
186
186
|
} catch (err) {
|
|
187
187
|
return rethrowUnexpected(err, `statUnder(${relPath})`);
|
|
188
188
|
}
|
|
@@ -190,7 +190,7 @@ export async function statUnder(root: string, relPath: string): Promise<fs.Stats
|
|
|
190
190
|
|
|
191
191
|
/** Ensure a directory exists under a root. */
|
|
192
192
|
export async function ensureDirUnder(root: string, relPath: string): Promise<void> {
|
|
193
|
-
await
|
|
193
|
+
await promises.mkdir(path.join(root, relPath), { recursive: true });
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
// ── Existence ───────────────────────────────────────────────────
|
|
@@ -201,7 +201,7 @@ export async function ensureDirUnder(root: string, relPath: string): Promise<voi
|
|
|
201
201
|
*/
|
|
202
202
|
export function existsInWorkspace(relPath: string): boolean {
|
|
203
203
|
try {
|
|
204
|
-
|
|
204
|
+
statSync(resolveWorkspacePath(relPath));
|
|
205
205
|
return true;
|
|
206
206
|
} catch (err) {
|
|
207
207
|
if (isEnoent(err)) return false;
|
|
@@ -217,5 +217,5 @@ export function existsInWorkspace(relPath: string): boolean {
|
|
|
217
217
|
* (including parents) if missing. Idempotent.
|
|
218
218
|
*/
|
|
219
219
|
export function ensureWorkspaceDir(relPath: string): void {
|
|
220
|
-
|
|
220
|
+
mkdirSync(resolveWorkspacePath(relPath), { recursive: true });
|
|
221
221
|
}
|
package/server/utils/gemini.ts
CHANGED
|
@@ -37,8 +37,8 @@ export async function generateGeminiImageContent(
|
|
|
37
37
|
config?: GenerateContentParameters["config"],
|
|
38
38
|
model: string = DEFAULT_IMAGE_MODEL,
|
|
39
39
|
): Promise<GeminiImageResult> {
|
|
40
|
-
const
|
|
41
|
-
const response = await
|
|
40
|
+
const client = getGeminiClient();
|
|
41
|
+
const response = await client.models.generateContent({
|
|
42
42
|
model,
|
|
43
43
|
contents,
|
|
44
44
|
...(config && { config }),
|
|
@@ -15,24 +15,24 @@
|
|
|
15
15
|
// the workspace scale (~hundreds of dirs) this is negligible. If
|
|
16
16
|
// profiling shows otherwise, cache the parsed ignore instances.
|
|
17
17
|
|
|
18
|
-
import
|
|
18
|
+
import { readFileSync } from "fs";
|
|
19
19
|
import path from "path";
|
|
20
20
|
import ignore, { type Ignore } from "ignore";
|
|
21
21
|
|
|
22
22
|
export class GitignoreFilter {
|
|
23
|
-
private
|
|
23
|
+
private rules: Ignore;
|
|
24
24
|
|
|
25
25
|
constructor(rules?: string) {
|
|
26
|
-
this.
|
|
26
|
+
this.rules = ignore();
|
|
27
27
|
if (rules) {
|
|
28
|
-
this.
|
|
28
|
+
this.rules.add(rules);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** Test whether a workspace-relative path should be hidden. */
|
|
33
33
|
ignores(relPath: string): boolean {
|
|
34
34
|
if (!relPath) return false;
|
|
35
|
-
return this.
|
|
35
|
+
return this.rules.ignores(relPath);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** Create a child filter that inherits this filter's rules and
|
|
@@ -40,12 +40,12 @@ export class GitignoreFilter {
|
|
|
40
40
|
childForDir(dirAbsPath: string): GitignoreFilter {
|
|
41
41
|
const child = new GitignoreFilter();
|
|
42
42
|
// Inherit parent rules
|
|
43
|
-
child.
|
|
43
|
+
child.rules = ignore().add(this.rules);
|
|
44
44
|
// Add local .gitignore if present
|
|
45
45
|
const gitignorePath = path.join(dirAbsPath, ".gitignore");
|
|
46
46
|
try {
|
|
47
|
-
const content =
|
|
48
|
-
child.
|
|
47
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
48
|
+
child.rules.add(content);
|
|
49
49
|
} catch {
|
|
50
50
|
// No .gitignore in this directory — just inherit parent
|
|
51
51
|
}
|
|
@@ -61,7 +61,7 @@ export class GitignoreFilter {
|
|
|
61
61
|
export function createRootFilter(workspaceRoot: string): GitignoreFilter {
|
|
62
62
|
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
63
63
|
try {
|
|
64
|
-
const content =
|
|
64
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
65
65
|
return new GitignoreFilter(content);
|
|
66
66
|
} catch {
|
|
67
67
|
return new GitignoreFilter();
|
package/server/utils/json.ts
CHANGED
|
@@ -44,22 +44,22 @@ export function findBalancedBraceBlock(raw: string): string | null {
|
|
|
44
44
|
let inString = false;
|
|
45
45
|
let escape = false;
|
|
46
46
|
for (let i = start; i < raw.length; i++) {
|
|
47
|
-
const
|
|
47
|
+
const char = raw[i];
|
|
48
48
|
if (escape) {
|
|
49
49
|
escape = false;
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
|
-
if (
|
|
52
|
+
if (char === "\\") {
|
|
53
53
|
escape = true;
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
|
-
if (
|
|
56
|
+
if (char === '"') {
|
|
57
57
|
inString = !inString;
|
|
58
58
|
continue;
|
|
59
59
|
}
|
|
60
60
|
if (inString) continue;
|
|
61
|
-
if (
|
|
62
|
-
if (
|
|
61
|
+
if (char === "{") depth++;
|
|
62
|
+
if (char === "}" && --depth === 0) return raw.slice(start, i + 1);
|
|
63
63
|
}
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { log } from "../system/logger/index.js";
|
|
2
|
+
import { errorMessage } from "./errors.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Build a `.catch` handler for a fire-and-forget background job that
|
|
@@ -8,15 +9,23 @@ import { log } from "../system/logger/index.js";
|
|
|
8
9
|
*
|
|
9
10
|
* Usage:
|
|
10
11
|
*
|
|
12
|
+
* // Default message — good for generic background scans.
|
|
11
13
|
* maybeRunJournal({ ... }).catch(logBackgroundError("journal"));
|
|
12
14
|
*
|
|
15
|
+
* // Custom message — pass context when the failure mode is
|
|
16
|
+
* // worth surfacing distinctly (e.g. "failed to register
|
|
17
|
+
* // scheduled skills" vs other `skills` warnings).
|
|
18
|
+
* registerScheduledSkills(...).catch(
|
|
19
|
+
* logBackgroundError("skills", "failed to register scheduled skills"),
|
|
20
|
+
* );
|
|
21
|
+
*
|
|
13
22
|
* The handler never rethrows — the caller's promise chain is
|
|
14
23
|
* terminated cleanly so nothing propagates into the request path.
|
|
15
24
|
*/
|
|
16
|
-
export function logBackgroundError(prefix: string): (err: unknown) => void {
|
|
25
|
+
export function logBackgroundError(prefix: string, message = "unexpected error in background"): (err: unknown) => void {
|
|
17
26
|
return (err) => {
|
|
18
|
-
log.warn(prefix,
|
|
19
|
-
error:
|
|
27
|
+
log.warn(prefix, message, {
|
|
28
|
+
error: errorMessage(err),
|
|
20
29
|
});
|
|
21
30
|
};
|
|
22
31
|
}
|
package/server/utils/markdown.ts
CHANGED
|
@@ -68,15 +68,15 @@ export function rewriteMarkdownLinks(input: string, rewrite: (href: string) => s
|
|
|
68
68
|
* Split a trailing `#fragment` or `?query` off a path so the caller
|
|
69
69
|
* can rewrite the path portion and concatenate the suffix back.
|
|
70
70
|
*/
|
|
71
|
-
export function splitFragmentAndQuery(
|
|
71
|
+
export function splitFragmentAndQuery(href: string): {
|
|
72
72
|
pathPart: string;
|
|
73
73
|
suffix: string;
|
|
74
74
|
} {
|
|
75
|
-
const hashIdx =
|
|
76
|
-
const queryIdx =
|
|
75
|
+
const hashIdx = href.indexOf("#");
|
|
76
|
+
const queryIdx = href.indexOf("?");
|
|
77
77
|
let cut = -1;
|
|
78
78
|
if (hashIdx !== -1) cut = hashIdx;
|
|
79
79
|
if (queryIdx !== -1 && (cut === -1 || queryIdx < cut)) cut = queryIdx;
|
|
80
|
-
if (cut === -1) return { pathPart:
|
|
81
|
-
return { pathPart:
|
|
80
|
+
if (cut === -1) return { pathPart: href, suffix: "" };
|
|
81
|
+
return { pathPart: href.slice(0, cut), suffix: href.slice(cut) };
|
|
82
82
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Type declarations for port.mjs. See port.mjs for rationale on why
|
|
2
|
+
// the shared helper lives in plain JS.
|
|
3
|
+
|
|
4
|
+
export const MAX_PORT_PROBES: number;
|
|
5
|
+
export function isPortFree(port: number): Promise<boolean>;
|
|
6
|
+
export function findAvailablePort(start: number): Promise<number | null>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Shared port-resolution helpers for the dev server and the npm
|
|
2
|
+
// launcher (`packages/mulmoclaude/bin/mulmoclaude.js`).
|
|
3
|
+
//
|
|
4
|
+
// Kept as plain `.mjs` (no TypeScript) because the launcher runs
|
|
5
|
+
// directly under Node — it boots BEFORE tsx is wired up, so it can't
|
|
6
|
+
// import from a `.ts` file. The server-side TypeScript imports this
|
|
7
|
+
// through Node's ESM resolution; `moduleResolution: "bundler"` + the
|
|
8
|
+
// sibling `port.d.mts` declarations give us the type coverage without
|
|
9
|
+
// turning the whole `server/` tree into mixed JS/TS.
|
|
10
|
+
|
|
11
|
+
import net from "node:net";
|
|
12
|
+
|
|
13
|
+
// Scan cap: 20 slots is enough to step around the occasional stale
|
|
14
|
+
// server on `localhost` without spinning forever on a pathologically
|
|
15
|
+
// saturated machine.
|
|
16
|
+
export const MAX_PORT_PROBES = 20;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns true iff binding `127.0.0.1:port` would succeed right now.
|
|
20
|
+
* @param {number} port
|
|
21
|
+
* @returns {Promise<boolean>}
|
|
22
|
+
*/
|
|
23
|
+
export function isPortFree(port) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const server = net.createServer();
|
|
26
|
+
server.once("error", () => resolve(false));
|
|
27
|
+
server.once("listening", () => {
|
|
28
|
+
server.close(() => resolve(true));
|
|
29
|
+
});
|
|
30
|
+
// Probe the same interface we'll actually bind to so a port
|
|
31
|
+
// held by a different process on a different interface doesn't
|
|
32
|
+
// give us a false "free" reading.
|
|
33
|
+
server.listen(port, "127.0.0.1");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Walk forward from `start` until we find a free port. Returns `null`
|
|
39
|
+
* if every slot in `[start, start + MAX_PORT_PROBES)` is busy.
|
|
40
|
+
* @param {number} start
|
|
41
|
+
* @returns {Promise<number | null>}
|
|
42
|
+
*/
|
|
43
|
+
export async function findAvailablePort(start) {
|
|
44
|
+
for (let candidate = start; candidate < start + MAX_PORT_PROBES; candidate++) {
|
|
45
|
+
if (await isPortFree(candidate)) return candidate;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|