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.
Files changed (251) hide show
  1. package/bin/mulmoclaude.js +7 -24
  2. package/client/assets/html2canvas-Cx501zZr-Cv5snK9D.js +5 -0
  3. package/client/assets/index-CubzmCVK.css +2 -0
  4. package/client/assets/{index-D8rhwXLq.js → index-DtcyExH9.js} +80 -61
  5. package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
  6. package/client/index.html +2 -4
  7. package/package.json +13 -13
  8. package/server/agent/attachmentConverter.ts +2 -2
  9. package/server/agent/config.ts +12 -12
  10. package/server/agent/index.ts +9 -3
  11. package/server/agent/mcp-server.ts +19 -19
  12. package/server/agent/mcp-tools/index.ts +6 -6
  13. package/server/agent/mcp-tools/x.ts +7 -6
  14. package/server/agent/prompt.ts +195 -29
  15. package/server/agent/resumeFailover.ts +5 -5
  16. package/server/agent/sandboxMounts.ts +10 -10
  17. package/server/agent/stream.ts +4 -4
  18. package/server/api/auth/bearerAuth.ts +3 -3
  19. package/server/api/auth/token.ts +2 -2
  20. package/server/api/routes/agent.ts +21 -3
  21. package/server/api/routes/config.ts +1 -1
  22. package/server/api/routes/files.ts +22 -21
  23. package/server/api/routes/html.ts +2 -2
  24. package/server/api/routes/image.ts +7 -7
  25. package/server/api/routes/mulmo-script.ts +33 -31
  26. package/server/api/routes/pdf.ts +2 -2
  27. package/server/api/routes/plugins.ts +16 -6
  28. package/server/api/routes/roles.ts +2 -2
  29. package/server/api/routes/scheduler.ts +14 -12
  30. package/server/api/routes/schedulerHandlers.ts +12 -12
  31. package/server/api/routes/schedulerTasks.ts +19 -17
  32. package/server/api/routes/sessions.ts +26 -26
  33. package/server/api/routes/sessionsCursor.ts +4 -4
  34. package/server/api/routes/skills.ts +5 -5
  35. package/server/api/routes/sources.ts +3 -3
  36. package/server/api/routes/todosColumnsHandlers.ts +30 -30
  37. package/server/api/routes/todosHandlers.ts +1 -1
  38. package/server/api/routes/todosItemsHandlers.ts +14 -14
  39. package/server/api/routes/wiki.ts +36 -22
  40. package/server/api/sandboxStatus.ts +1 -1
  41. package/server/events/notifications.ts +6 -6
  42. package/server/events/pub-sub/index.ts +3 -3
  43. package/server/events/relay-client.ts +17 -16
  44. package/server/events/scheduler-adapter.ts +20 -20
  45. package/server/events/session-store/index.ts +10 -10
  46. package/server/events/task-manager/index.ts +7 -7
  47. package/server/index.ts +59 -65
  48. package/server/system/config.ts +5 -5
  49. package/server/system/credentials.ts +7 -5
  50. package/server/system/env.ts +5 -5
  51. package/server/utils/date.ts +18 -18
  52. package/server/utils/files/atomic.ts +16 -16
  53. package/server/utils/files/html-io.ts +5 -5
  54. package/server/utils/files/image-store.ts +19 -8
  55. package/server/utils/files/journal-io.ts +4 -4
  56. package/server/utils/files/json.ts +5 -5
  57. package/server/utils/files/markdown-store.ts +4 -4
  58. package/server/utils/files/naming.ts +2 -2
  59. package/server/utils/files/reference-dirs-io.ts +3 -3
  60. package/server/utils/files/roles-io.ts +12 -12
  61. package/server/utils/files/safe.ts +14 -14
  62. package/server/utils/files/scheduler-io.ts +5 -5
  63. package/server/utils/files/scheduler-overrides-io.ts +2 -2
  64. package/server/utils/files/session-io.ts +35 -35
  65. package/server/utils/files/spreadsheet-store.ts +7 -7
  66. package/server/utils/files/todos-io.ts +9 -9
  67. package/server/utils/files/user-tasks-io.ts +5 -5
  68. package/server/utils/files/workspace-io.ts +12 -12
  69. package/server/utils/gemini.ts +2 -2
  70. package/server/utils/gitignore.ts +9 -9
  71. package/server/utils/json.ts +5 -5
  72. package/server/utils/logBackgroundError.ts +12 -3
  73. package/server/utils/markdown.ts +5 -5
  74. package/server/utils/port.d.mts +6 -0
  75. package/server/utils/port.mjs +48 -0
  76. package/server/utils/request.ts +12 -6
  77. package/server/utils/spawn.ts +1 -1
  78. package/server/utils/types.ts +2 -2
  79. package/server/workspace/chat-index/indexer.ts +15 -15
  80. package/server/workspace/chat-index/summarizer.ts +4 -4
  81. package/server/workspace/custom-dirs.ts +16 -16
  82. package/server/workspace/journal/archivist.ts +35 -35
  83. package/server/workspace/journal/dailyPass.ts +31 -28
  84. package/server/workspace/journal/diff.ts +2 -2
  85. package/server/workspace/journal/index.ts +4 -4
  86. package/server/workspace/journal/indexFile.ts +29 -25
  87. package/server/workspace/journal/optimizationPass.ts +2 -2
  88. package/server/workspace/journal/state.ts +6 -6
  89. package/server/workspace/paths.ts +3 -3
  90. package/server/workspace/reference-dirs.ts +20 -20
  91. package/server/workspace/roles.ts +6 -6
  92. package/server/workspace/skills/discovery.ts +4 -4
  93. package/server/workspace/skills/parser.ts +6 -6
  94. package/server/workspace/skills/scheduler.ts +3 -3
  95. package/server/workspace/skills/user-tasks.ts +34 -34
  96. package/server/workspace/skills/writer.ts +3 -3
  97. package/server/workspace/sources/arxivDiscovery.ts +10 -10
  98. package/server/workspace/sources/classifier.ts +7 -7
  99. package/server/workspace/sources/fetchers/arxiv.ts +7 -7
  100. package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
  101. package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
  102. package/server/workspace/sources/fetchers/rss.ts +5 -5
  103. package/server/workspace/sources/fetchers/rssParser.ts +4 -4
  104. package/server/workspace/sources/interests.ts +12 -12
  105. package/server/workspace/sources/paths.ts +6 -6
  106. package/server/workspace/sources/pipeline/fetch.ts +36 -13
  107. package/server/workspace/sources/pipeline/index.ts +8 -13
  108. package/server/workspace/sources/pipeline/notify.ts +3 -3
  109. package/server/workspace/sources/pipeline/plan.ts +15 -13
  110. package/server/workspace/sources/pipeline/write.ts +5 -5
  111. package/server/workspace/sources/rateLimiter.ts +1 -1
  112. package/server/workspace/sources/registry.ts +16 -16
  113. package/server/workspace/sources/robots.ts +14 -14
  114. package/server/workspace/sources/sourceState.ts +17 -10
  115. package/server/workspace/sources/types.ts +9 -0
  116. package/server/workspace/sources/urls.ts +1 -1
  117. package/server/workspace/tool-trace/classify.ts +4 -4
  118. package/server/workspace/tool-trace/index.ts +1 -1
  119. package/server/workspace/tool-trace/writeSearch.ts +26 -16
  120. package/server/workspace/wiki-backlinks/index.ts +8 -8
  121. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
  122. package/server/workspace/workspace.ts +7 -7
  123. package/src/App.vue +315 -141
  124. package/src/components/CanvasViewToggle.vue +10 -7
  125. package/src/components/ChatInput.vue +67 -33
  126. package/src/components/FileContentHeader.vue +7 -4
  127. package/src/components/FileContentRenderer.vue +20 -6
  128. package/src/components/FileTree.vue +6 -3
  129. package/src/components/FileTreePane.vue +11 -8
  130. package/src/components/FilesView.vue +5 -3
  131. package/src/components/LockStatusPopup.vue +17 -14
  132. package/src/components/NotificationBell.vue +14 -5
  133. package/src/components/NotificationToast.vue +6 -3
  134. package/src/components/PluginLauncher.vue +19 -56
  135. package/src/components/RightSidebar.vue +13 -10
  136. package/src/components/RoleSelector.vue +2 -2
  137. package/src/components/SessionHistoryPanel.vue +38 -34
  138. package/src/components/SessionTabBar.vue +8 -10
  139. package/src/components/SettingsMcpTab.vue +49 -36
  140. package/src/components/SettingsModal.vue +24 -22
  141. package/src/components/SettingsReferenceDirsTab.vue +39 -34
  142. package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
  143. package/src/components/SidebarHeader.vue +25 -4
  144. package/src/components/StackView.vue +4 -1
  145. package/src/components/SuggestionsPanel.vue +7 -4
  146. package/src/components/TodoExplorer.vue +26 -15
  147. package/src/components/ToolResultsPanel.vue +27 -13
  148. package/src/components/todo/TodoAddDialog.vue +19 -14
  149. package/src/components/todo/TodoEditDialog.vue +7 -2
  150. package/src/components/todo/TodoEditPanel.vue +17 -12
  151. package/src/components/todo/TodoKanbanView.vue +10 -5
  152. package/src/components/todo/TodoListView.vue +10 -7
  153. package/src/components/todo/TodoTableView.vue +5 -2
  154. package/src/composables/useAppApi.ts +9 -0
  155. package/src/composables/useClickOutside.ts +2 -2
  156. package/src/composables/useDynamicFavicon.ts +172 -37
  157. package/src/composables/useEventListeners.ts +7 -8
  158. package/src/composables/useFaviconState.ts +13 -2
  159. package/src/composables/useFileSelection.ts +24 -6
  160. package/src/composables/useFreshPluginData.ts +3 -3
  161. package/src/composables/useKeyNavigation.ts +11 -11
  162. package/src/composables/useLayoutMode.ts +32 -0
  163. package/src/composables/useMcpTools.ts +2 -2
  164. package/src/composables/useNotifications.ts +3 -3
  165. package/src/composables/usePdfDownload.ts +4 -4
  166. package/src/composables/usePendingCalls.ts +1 -1
  167. package/src/composables/usePubSub.ts +10 -10
  168. package/src/composables/useRoles.ts +1 -1
  169. package/src/composables/useSandboxStatus.ts +1 -1
  170. package/src/composables/useSessionDerived.ts +3 -3
  171. package/src/composables/useSessionHistory.ts +7 -17
  172. package/src/composables/useSessionSync.ts +8 -8
  173. package/src/composables/useViewLayout.ts +20 -34
  174. package/src/config/roles.ts +2 -2
  175. package/src/lang/de.ts +536 -0
  176. package/src/lang/en.ts +558 -0
  177. package/src/lang/es.ts +543 -0
  178. package/src/lang/fr.ts +536 -0
  179. package/src/lang/ja.ts +536 -0
  180. package/src/lang/ko.ts +540 -0
  181. package/src/lang/pt-BR.ts +534 -0
  182. package/src/lang/zh.ts +537 -0
  183. package/src/lib/vue-i18n.ts +97 -0
  184. package/src/main.ts +2 -0
  185. package/src/plugins/canvas/View.vue +102 -186
  186. package/src/plugins/canvas/definition.ts +0 -8
  187. package/src/plugins/chart/Preview.vue +5 -5
  188. package/src/plugins/chart/View.vue +9 -4
  189. package/src/plugins/manageRoles/Preview.vue +4 -1
  190. package/src/plugins/manageRoles/View.vue +59 -43
  191. package/src/plugins/manageSkills/Preview.vue +8 -3
  192. package/src/plugins/manageSkills/View.vue +29 -25
  193. package/src/plugins/manageSource/Preview.vue +2 -2
  194. package/src/plugins/manageSource/View.vue +73 -52
  195. package/src/plugins/markdown/Preview.vue +1 -1
  196. package/src/plugins/markdown/View.vue +26 -36
  197. package/src/plugins/presentHtml/Preview.vue +1 -1
  198. package/src/plugins/presentHtml/View.vue +7 -4
  199. package/src/plugins/presentHtml/helpers.ts +8 -8
  200. package/src/plugins/presentMulmoScript/Preview.vue +1 -1
  201. package/src/plugins/presentMulmoScript/View.vue +40 -30
  202. package/src/plugins/presentMulmoScript/helpers.ts +1 -1
  203. package/src/plugins/scheduler/Preview.vue +13 -10
  204. package/src/plugins/scheduler/TasksTab.vue +57 -28
  205. package/src/plugins/scheduler/View.vue +28 -19
  206. package/src/plugins/scheduler/formatSchedule.ts +93 -0
  207. package/src/plugins/spreadsheet/Preview.vue +8 -3
  208. package/src/plugins/spreadsheet/View.vue +21 -12
  209. package/src/plugins/textResponse/Preview.vue +15 -58
  210. package/src/plugins/textResponse/View.vue +29 -9
  211. package/src/plugins/todo/Preview.vue +13 -8
  212. package/src/plugins/todo/View.vue +38 -24
  213. package/src/plugins/todo/composables/useTodos.ts +5 -5
  214. package/src/plugins/ui-image/ImagePreview.vue +6 -3
  215. package/src/plugins/ui-image/ImageView.vue +7 -4
  216. package/src/plugins/wiki/Preview.vue +10 -7
  217. package/src/plugins/wiki/View.vue +202 -81
  218. package/src/plugins/wiki/helpers.ts +4 -4
  219. package/src/plugins/wiki/route.ts +112 -0
  220. package/src/router/guards.ts +46 -28
  221. package/src/router/index.ts +41 -26
  222. package/src/types/session.ts +4 -3
  223. package/src/types/vue-i18n.d.ts +20 -0
  224. package/src/utils/agent/request.ts +22 -3
  225. package/src/utils/canvas/layoutMode.ts +26 -0
  226. package/src/utils/dom/scrollable.ts +2 -2
  227. package/src/utils/files/expandedDirs.ts +1 -1
  228. package/src/utils/files/sortChildren.ts +6 -6
  229. package/src/utils/format/frontmatter.ts +6 -6
  230. package/src/utils/image/cacheBust.ts +16 -0
  231. package/src/utils/image/resolve.ts +16 -0
  232. package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
  233. package/src/utils/markdown/extractFirstH1.ts +2 -2
  234. package/src/utils/path/relativeLink.ts +15 -15
  235. package/src/utils/path/workspaceLinkRouter.ts +81 -0
  236. package/src/utils/role/icon.ts +2 -2
  237. package/src/utils/role/merge.ts +2 -2
  238. package/src/utils/role/plugins.ts +1 -1
  239. package/src/utils/session/sessionFactory.ts +2 -2
  240. package/src/utils/session/sessionHelpers.ts +2 -2
  241. package/src/utils/tools/dedup.ts +4 -4
  242. package/src/utils/tools/result.ts +3 -3
  243. package/src/utils/types.ts +2 -2
  244. package/src/vite-env.d.ts +9 -0
  245. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
  246. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
  247. package/client/assets/index-KNLBjwuh.css +0 -1
  248. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
  249. package/src/composables/useCanvasViewMode.ts +0 -121
  250. package/src/utils/canvas/viewMode.ts +0 -46
  251. /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
@@ -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 d = typeof input === "number" ? new Date(input) : input;
12
- const y = d.getFullYear();
13
- const m = String(d.getMonth() + 1).padStart(2, "0");
14
- const day = String(d.getDate()).padStart(2, "0");
15
- return `${y}-${m}-${day}`;
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(ts: Date): string {
24
- const y = ts.getUTCFullYear();
25
- const m = String(ts.getUTCMonth() + 1).padStart(2, "0");
26
- const d = String(ts.getUTCDate()).padStart(2, "0");
27
- return `${y}-${m}-${d}`;
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(s: string): boolean {
45
- if (s.length !== 10) return false;
46
- if (s[4] !== "-" || s[7] !== "-") return false;
47
- return isNumeric(s.slice(0, 4)) && isNumeric(s.slice(5, 7)) && isNumeric(s.slice(8, 10));
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(s: string): boolean {
51
- if (s.length === 0) return false;
52
- for (let i = 0; i < s.length; i++) {
53
- const code = s.charCodeAt(i);
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 fs from "fs";
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(from: string, to: string): Promise<void> {
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 fs.promises.rename(from, to);
53
+ await promises.rename(fromPath, toPath);
54
54
  return;
55
55
  } catch (err) {
56
56
  if (!isTransientRenameError(err)) throw err;
57
- await new Promise((r) => setTimeout(r, delayMs));
57
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
58
58
  }
59
59
  }
60
60
  // Final attempt — let any error propagate.
61
- await fs.promises.rename(from, to);
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(ms: number): void {
70
- Atomics.wait(SYNC_SLEEP_BUF, 0, 0, ms);
69
+ function sleepSync(millis: number): void {
70
+ Atomics.wait(SYNC_SLEEP_BUF, 0, 0, millis);
71
71
  }
72
72
 
73
- function renameSyncWithWindowsRetry(from: string, to: string): void {
73
+ function renameSyncWithWindowsRetry(fromPath: string, toPath: string): void {
74
74
  for (const delayMs of RENAME_RETRY_DELAYS_MS) {
75
75
  try {
76
- fs.renameSync(from, to);
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
- fs.renameSync(from, to);
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 fs.promises.mkdir(path.dirname(filePath), { recursive: true });
93
+ await promises.mkdir(path.dirname(filePath), { recursive: true });
94
94
  try {
95
- await fs.promises.writeFile(tmp, content, {
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 fs.promises.unlink(tmp).catch(() => {});
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
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
113
+ mkdirSync(path.dirname(filePath), { recursive: true });
114
114
  try {
115
- fs.writeFileSync(tmp, content, { encoding: "utf-8", mode: opts.mode });
115
+ writeFileSync(tmp, content, { encoding: "utf-8", mode: opts.mode });
116
116
  renameSyncWithWindowsRetry(tmp, filePath);
117
117
  } catch (err) {
118
118
  try {
119
- fs.unlinkSync(tmp);
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 = (r?: string) => r ?? workspacePath;
12
+ const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
13
13
 
14
- export async function readCurrentHtml(r?: string): Promise<string | null> {
15
- return readTextUnder(root(r), HTML_REL);
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, r?: string): Promise<void> {
19
- await writeTextUnder(root(r), HTML_REL, html);
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 fs from "fs/promises";
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 fs.mkdir(IMAGES_DIR, { recursive: true });
16
- imagesDirReal = await fs.realpath(IMAGES_DIR);
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 id = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
39
- const filename = `${id}.png`;
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 fs.writeFile(absPath, Buffer.from(base64Data, "base64"));
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 fs.writeFile(absPath, Buffer.from(base64Data, "base64"));
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 fs.readFile(absPath);
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 fs from "node:fs";
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
- fs.statSync(filePath);
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(md: string, rootOverride?: string): Promise<void> {
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, md);
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 fs from "fs";
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 = fs.readFileSync(filePath, "utf-8");
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
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
45
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
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 fs.promises.readFile(filePath, "utf-8");
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 fs from "fs/promises";
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 fs.writeFile(path.join(workspacePath, relPath), content, "utf-8");
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 fs.readFile(absPath, "utf-8");
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 fs.writeFile(absPath, content, "utf-8");
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 id = crypto.randomUUID().replace(/-/g, "").slice(0, RANDOM_SUFFIX_LEN);
48
- const fname = `${slug}-${id}${ext}`;
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 fs from "fs";
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
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
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 fs.statSync(hostPath).isDirectory();
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 fs from "node:fs";
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 = (r?: string) => r ?? workspacePath;
13
+ const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
14
14
 
15
- function roleFilePath(id: string, r?: string): string {
16
- return path.join(root(r), WORKSPACE_DIRS.roles, `${id}.json`);
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(id: string, r?: string): boolean {
20
+ export function roleExists(roleId: string, workspaceRoot?: string): boolean {
21
21
  try {
22
- fs.statSync(roleFilePath(id, r));
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(id: string, r?: string): boolean {
30
+ export function deleteRole(roleId: string, workspaceRoot?: string): boolean {
31
31
  try {
32
- fs.unlinkSync(roleFilePath(id, r));
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(id: string, data: unknown, r?: string): void {
42
- const dir = path.join(root(r), WORKSPACE_DIRS.roles);
43
- fs.mkdirSync(dir, { recursive: true });
44
- writeFileAtomicSync(roleFilePath(id, r), JSON.stringify(data, null, 2));
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 fs from "fs";
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 fs.readFileSync(absPath);
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 fs.promises.readFile(absPath, "utf-8");
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 fs.readFileSync(absPath, "utf-8");
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): fs.Stats | null {
46
+ export function statSafe(absPath: string): Stats | null {
47
47
  try {
48
- return fs.statSync(absPath);
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<fs.Stats | null> {
54
+ export async function statSafeAsync(absPath: string): Promise<Stats | null> {
55
55
  try {
56
- return await fs.promises.stat(absPath);
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): fs.Dirent[] {
62
+ export function readDirSafe(absPath: string): Dirent[] {
63
63
  try {
64
- return fs.readdirSync(absPath, { withFileTypes: true });
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<fs.Dirent[]> {
70
+ export async function readDirSafeAsync(absPath: string): Promise<Dirent[]> {
71
71
  try {
72
- return await fs.promises.readdir(absPath, { withFileTypes: true });
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 fs.promises.readFile(file, "utf-8");
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 = fs.realpathSync(resolved);
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 = (r?: string) => r ?? workspacePath;
12
+ const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
13
13
 
14
- export function loadSchedulerItems<T>(fallback: T, r?: string): T {
15
- return loadJsonFile(resolvePath(root(r), WORKSPACE_FILES.schedulerItems), fallback);
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, r?: string): void {
19
- writeFileAtomicSync(resolvePath(root(r), WORKSPACE_FILES.schedulerItems), JSON.stringify(items, null, 2));
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 fs from "fs";
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
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
62
+ mkdirSync(path.dirname(filePath), { recursive: true });
63
63
  writeFileAtomicSync(filePath, JSON.stringify(overrides, null, 2));
64
64
  }