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
@@ -18,12 +18,12 @@ export function ensureChatDir(): void {
18
18
  ensureWorkspaceDir(CHAT);
19
19
  }
20
20
 
21
- function metaRel(id: string): string {
22
- return path.posix.join(CHAT, `${id}.json`);
21
+ function metaRel(sessionId: string): string {
22
+ return path.posix.join(CHAT, `${sessionId}.json`);
23
23
  }
24
24
 
25
- function jsonlRel(id: string): string {
26
- return path.posix.join(CHAT, `${id}.jsonl`);
25
+ function jsonlRel(sessionId: string): string {
26
+ return path.posix.join(CHAT, `${sessionId}.jsonl`);
27
27
  }
28
28
 
29
29
  // ── Meta ────────────────────────────────────────────────────────
@@ -41,8 +41,8 @@ export interface SessionMeta {
41
41
  export type ReadMetaResult = { kind: "missing" } | { kind: "ok"; meta: SessionMeta } | { kind: "corrupt"; raw: string };
42
42
 
43
43
  /** Read session metadata with full outcome discrimination. */
44
- export async function readSessionMetaFull(id: string, rootOverride?: string): Promise<ReadMetaResult> {
45
- const raw = await readTextUnder(root(rootOverride), metaRel(id));
44
+ export async function readSessionMetaFull(sessionId: string, rootOverride?: string): Promise<ReadMetaResult> {
45
+ const raw = await readTextUnder(root(rootOverride), metaRel(sessionId));
46
46
  if (raw === null) return { kind: "missing" };
47
47
  try {
48
48
  return { kind: "ok", meta: JSON.parse(raw) as SessionMeta };
@@ -53,60 +53,60 @@ export async function readSessionMetaFull(id: string, rootOverride?: string): Pr
53
53
 
54
54
  /** Convenience: returns the meta or null. Treats corrupt as null
55
55
  * (callers that need to distinguish use readSessionMetaFull). */
56
- export async function readSessionMeta(id: string, rootOverride?: string): Promise<SessionMeta | null> {
57
- const result = await readSessionMetaFull(id, rootOverride);
56
+ export async function readSessionMeta(sessionId: string, rootOverride?: string): Promise<SessionMeta | null> {
57
+ const result = await readSessionMetaFull(sessionId, rootOverride);
58
58
  return result.kind === "ok" ? result.meta : null;
59
59
  }
60
60
 
61
- export async function writeSessionMeta(id: string, meta: SessionMeta, rootOverride?: string): Promise<void> {
62
- await writeTextUnder(root(rootOverride), metaRel(id), JSON.stringify(meta, null, 2));
61
+ export async function writeSessionMeta(sessionId: string, meta: SessionMeta, rootOverride?: string): Promise<void> {
62
+ await writeTextUnder(root(rootOverride), metaRel(sessionId), JSON.stringify(meta, null, 2));
63
63
  }
64
64
 
65
- export async function createSessionMeta(id: string, roleId: string, firstUserMessage: string, rootOverride?: string, origin?: string): Promise<void> {
65
+ export async function createSessionMeta(sessionId: string, roleId: string, firstUserMessage: string, rootOverride?: string, origin?: string): Promise<void> {
66
66
  const meta: Record<string, unknown> = {
67
67
  roleId,
68
68
  startedAt: new Date().toISOString(),
69
69
  firstUserMessage,
70
70
  };
71
71
  if (origin) meta.origin = origin;
72
- await writeSessionMeta(id, meta, rootOverride);
72
+ await writeSessionMeta(sessionId, meta, rootOverride);
73
73
  }
74
74
 
75
- export async function backfillOrigin(id: string, origin: SessionMeta["origin"], rootOverride?: string): Promise<void> {
76
- const meta = await readSessionMeta(id, rootOverride);
75
+ export async function backfillOrigin(sessionId: string, origin: SessionMeta["origin"], rootOverride?: string): Promise<void> {
76
+ const meta = await readSessionMeta(sessionId, rootOverride);
77
77
  if (!meta || meta.origin) return; // already set
78
- await writeSessionMeta(id, { ...meta, origin }, rootOverride);
78
+ await writeSessionMeta(sessionId, { ...meta, origin }, rootOverride);
79
79
  }
80
80
 
81
- export async function backfillFirstUserMessage(id: string, message: string, rootOverride?: string): Promise<void> {
82
- const meta = await readSessionMeta(id, rootOverride);
81
+ export async function backfillFirstUserMessage(sessionId: string, message: string, rootOverride?: string): Promise<void> {
82
+ const meta = await readSessionMeta(sessionId, rootOverride);
83
83
  if (!meta || meta.firstUserMessage) return;
84
- await writeSessionMeta(id, { ...meta, firstUserMessage: message }, rootOverride);
84
+ await writeSessionMeta(sessionId, { ...meta, firstUserMessage: message }, rootOverride);
85
85
  }
86
86
 
87
- export async function setClaudeSessionId(id: string, claudeSessionId: string, rootOverride?: string): Promise<void> {
88
- const meta = await readSessionMeta(id, rootOverride);
87
+ export async function setClaudeSessionId(sessionId: string, claudeSessionId: string, rootOverride?: string): Promise<void> {
88
+ const meta = await readSessionMeta(sessionId, rootOverride);
89
89
  if (!meta) return;
90
- await writeSessionMeta(id, { ...meta, claudeSessionId }, rootOverride);
90
+ await writeSessionMeta(sessionId, { ...meta, claudeSessionId }, rootOverride);
91
91
  }
92
92
 
93
- export async function clearClaudeSessionId(id: string, rootOverride?: string): Promise<void> {
94
- const meta = await readSessionMeta(id, rootOverride);
93
+ export async function clearClaudeSessionId(sessionId: string, rootOverride?: string): Promise<void> {
94
+ const meta = await readSessionMeta(sessionId, rootOverride);
95
95
  if (!meta) return;
96
96
  const { claudeSessionId: __removed, ...rest } = meta;
97
- await writeSessionMeta(id, rest, rootOverride);
97
+ await writeSessionMeta(sessionId, rest, rootOverride);
98
98
  }
99
99
 
100
- export async function updateHasUnread(id: string, hasUnread: boolean, rootOverride?: string): Promise<void> {
101
- const meta = await readSessionMeta(id, rootOverride);
100
+ export async function updateHasUnread(sessionId: string, hasUnread: boolean, rootOverride?: string): Promise<void> {
101
+ const meta = await readSessionMeta(sessionId, rootOverride);
102
102
  if (!meta) return;
103
- await writeSessionMeta(id, { ...meta, hasUnread }, rootOverride);
103
+ await writeSessionMeta(sessionId, { ...meta, hasUnread }, rootOverride);
104
104
  }
105
105
 
106
106
  // ── Jsonl ───────────────────────────────────────────────────────
107
107
 
108
- export function sessionJsonlAbsPath(id: string, rootOverride?: string): string {
109
- return resolvePath(root(rootOverride), jsonlRel(id));
108
+ export function sessionJsonlAbsPath(sessionId: string, rootOverride?: string): string {
109
+ return resolvePath(root(rootOverride), jsonlRel(sessionId));
110
110
  }
111
111
 
112
112
  /**
@@ -115,12 +115,12 @@ export function sessionJsonlAbsPath(id: string, rootOverride?: string): string {
115
115
  * `hasUnread`, `roleId`, `startedAt`, `origin`, etc. Its mtime bumps
116
116
  * whenever any of those fields change via `writeSessionMeta`.
117
117
  */
118
- export function sessionMetaAbsPath(id: string, rootOverride?: string): string {
119
- return resolvePath(root(rootOverride), metaRel(id));
118
+ export function sessionMetaAbsPath(sessionId: string, rootOverride?: string): string {
119
+ return resolvePath(root(rootOverride), metaRel(sessionId));
120
120
  }
121
121
 
122
- export async function readSessionJsonl(id: string, rootOverride?: string): Promise<string | null> {
123
- return readTextUnder(root(rootOverride), jsonlRel(id));
122
+ export async function readSessionJsonl(sessionId: string, rootOverride?: string): Promise<string | null> {
123
+ return readTextUnder(root(rootOverride), jsonlRel(sessionId));
124
124
  }
125
125
 
126
126
  /**
@@ -130,7 +130,7 @@ export async function readSessionJsonl(id: string, rootOverride?: string): Promi
130
130
  * content and don't need to worry about line termination. This
131
131
  * prevents JSONL parse failures from missing newlines.
132
132
  */
133
- export async function appendSessionLine(id: string, line: string, rootOverride?: string): Promise<void> {
133
+ export async function appendSessionLine(sessionId: string, line: string, rootOverride?: string): Promise<void> {
134
134
  const normalized = line.endsWith("\n") ? line : `${line}\n`;
135
- await appendFile(resolvePath(root(rootOverride), jsonlRel(id)), normalized);
135
+ await appendFile(resolvePath(root(rootOverride), jsonlRel(sessionId)), normalized);
136
136
  }
@@ -1,4 +1,4 @@
1
- import fs from "fs/promises";
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 fs.mkdir(SPREADSHEETS_DIR, { recursive: true });
17
- spreadsheetsDirReal = await fs.realpath(SPREADSHEETS_DIR);
16
+ await mkdir(SPREADSHEETS_DIR, { recursive: true });
17
+ spreadsheetsDirReal = await realpath(SPREADSHEETS_DIR);
18
18
  return spreadsheetsDirReal;
19
19
  }
20
20
 
@@ -36,16 +36,16 @@ async function safeResolve(relativePath: string): Promise<string> {
36
36
  /** Save sheets array as a JSON file. Returns the workspace-relative path. */
37
37
  export async function saveSpreadsheet(sheets: unknown[]): Promise<string> {
38
38
  await ensureSpreadsheetsDir();
39
- const id = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
40
- const filename = `${id}.json`;
41
- await fs.writeFile(path.join(SPREADSHEETS_DIR, filename), JSON.stringify(sheets), "utf-8");
39
+ const sheetId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
40
+ const filename = `${sheetId}.json`;
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 fs.writeFile(absPath, JSON.stringify(sheets), "utf-8");
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).
@@ -10,20 +10,20 @@ import { resolvePath } from "./workspace-io.js";
10
10
  import { loadJsonFile } from "./json.js";
11
11
  import { writeFileAtomicSync } from "./atomic.js";
12
12
 
13
- const root = (r?: string) => r ?? workspacePath;
13
+ const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
14
14
 
15
- export function loadTodos<T>(fallback: T, r?: string): T {
16
- return loadJsonFile(resolvePath(root(r), WORKSPACE_FILES.todosItems), fallback);
15
+ export function loadTodos<T>(fallback: T, workspaceRoot?: string): T {
16
+ return loadJsonFile(resolvePath(root(workspaceRoot), WORKSPACE_FILES.todosItems), fallback);
17
17
  }
18
18
 
19
- export function saveTodos(items: unknown, r?: string): void {
20
- writeFileAtomicSync(resolvePath(root(r), WORKSPACE_FILES.todosItems), JSON.stringify(items, null, 2));
19
+ export function saveTodos(items: unknown, workspaceRoot?: string): void {
20
+ writeFileAtomicSync(resolvePath(root(workspaceRoot), WORKSPACE_FILES.todosItems), JSON.stringify(items, null, 2));
21
21
  }
22
22
 
23
- export function loadColumns<T>(fallback: T, r?: string): T {
24
- return loadJsonFile(resolvePath(root(r), WORKSPACE_FILES.todosColumns), fallback);
23
+ export function loadColumns<T>(fallback: T, workspaceRoot?: string): T {
24
+ return loadJsonFile(resolvePath(root(workspaceRoot), WORKSPACE_FILES.todosColumns), fallback);
25
25
  }
26
26
 
27
- export function saveColumns(columns: unknown, r?: string): void {
28
- writeFileAtomicSync(resolvePath(root(r), WORKSPACE_FILES.todosColumns), JSON.stringify(columns, null, 2));
27
+ export function saveColumns(columns: unknown, workspaceRoot?: string): void {
28
+ writeFileAtomicSync(resolvePath(root(workspaceRoot), WORKSPACE_FILES.todosColumns), JSON.stringify(columns, null, 2));
29
29
  }
@@ -11,15 +11,15 @@ import { resolvePath } from "./workspace-io.js";
11
11
  import { loadJsonFile } from "./json.js";
12
12
  import { writeFileAtomic } from "./atomic.js";
13
13
 
14
- const root = (r?: string) => r ?? workspacePath;
14
+ const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
15
15
 
16
- export function loadUserTasks<T>(r?: string): T[] {
17
- const tasks = loadJsonFile<T[]>(resolvePath(root(r), WORKSPACE_FILES.schedulerUserTasks), []);
16
+ export function loadUserTasks<T>(workspaceRoot?: string): T[] {
17
+ const tasks = loadJsonFile<T[]>(resolvePath(root(workspaceRoot), WORKSPACE_FILES.schedulerUserTasks), []);
18
18
  return Array.isArray(tasks) ? tasks : [];
19
19
  }
20
20
 
21
- export async function saveUserTasks<T>(tasks: T[], r?: string): Promise<void> {
22
- const filePath = resolvePath(root(r), WORKSPACE_FILES.schedulerUserTasks);
21
+ export async function saveUserTasks<T>(tasks: T[], workspaceRoot?: string): Promise<void> {
22
+ const filePath = resolvePath(root(workspaceRoot), WORKSPACE_FILES.schedulerUserTasks);
23
23
  await mkdir(path.dirname(filePath), { recursive: true });
24
24
  await writeFileAtomic(filePath, JSON.stringify(tasks, null, 2));
25
25
  }
@@ -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 fs from "fs";
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 fs.promises.readFile(resolveWorkspacePath(relPath), "utf-8");
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 fs.readFileSync(resolveWorkspacePath(relPath), "utf-8");
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 fs.promises.readFile(path.join(root, relPath), "utf-8");
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 fs.readFileSync(path.join(root, relPath), "utf-8");
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 fs.readdirSync(path.join(root, relPath));
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 fs.promises.readdir(path.join(root, relPath));
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<fs.Stats | null> {
183
+ export async function statUnder(root: string, relPath: string): Promise<Stats | null> {
184
184
  try {
185
- return await fs.promises.stat(path.join(root, relPath));
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 fs.promises.mkdir(path.join(root, relPath), { recursive: true });
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
- fs.statSync(resolveWorkspacePath(relPath));
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
- fs.mkdirSync(resolveWorkspacePath(relPath), { recursive: true });
220
+ mkdirSync(resolveWorkspacePath(relPath), { recursive: true });
221
221
  }
@@ -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 ai = getGeminiClient();
41
- const response = await ai.models.generateContent({
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 fs from "fs";
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 ig: Ignore;
23
+ private rules: Ignore;
24
24
 
25
25
  constructor(rules?: string) {
26
- this.ig = ignore();
26
+ this.rules = ignore();
27
27
  if (rules) {
28
- this.ig.add(rules);
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.ig.ignores(relPath);
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.ig = ignore().add(this.ig);
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 = fs.readFileSync(gitignorePath, "utf-8");
48
- child.ig.add(content);
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 = fs.readFileSync(gitignorePath, "utf-8");
64
+ const content = readFileSync(gitignorePath, "utf-8");
65
65
  return new GitignoreFilter(content);
66
66
  } catch {
67
67
  return new GitignoreFilter();
@@ -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 ch = raw[i];
47
+ const char = raw[i];
48
48
  if (escape) {
49
49
  escape = false;
50
50
  continue;
51
51
  }
52
- if (ch === "\\") {
52
+ if (char === "\\") {
53
53
  escape = true;
54
54
  continue;
55
55
  }
56
- if (ch === '"') {
56
+ if (char === '"') {
57
57
  inString = !inString;
58
58
  continue;
59
59
  }
60
60
  if (inString) continue;
61
- if (ch === "{") depth++;
62
- if (ch === "}" && --depth === 0) return raw.slice(start, i + 1);
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, "unexpected error in background", {
19
- error: String(err),
27
+ log.warn(prefix, message, {
28
+ error: errorMessage(err),
20
29
  });
21
30
  };
22
31
  }
@@ -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(s: string): {
71
+ export function splitFragmentAndQuery(href: string): {
72
72
  pathPart: string;
73
73
  suffix: string;
74
74
  } {
75
- const hashIdx = s.indexOf("#");
76
- const queryIdx = s.indexOf("?");
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: s, suffix: "" };
81
- return { pathPart: s.slice(0, cut), suffix: s.slice(cut) };
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
+ }
@@ -3,11 +3,17 @@
3
3
  // Centralizes patterns that were duplicated across route handlers
4
4
  // (3+ different ways to read `req.query.session`).
5
5
 
6
- // Use a minimal interface so the helpers work with any Express
7
- // Request generic (Request<object, ...>, Request<Params, ...>, etc.)
8
- // without type incompatibility.
6
+ // `query: object` so the helpers work with any Express Request
7
+ // generic — `Request<Params, ResBody, ReqBody, Query>`. A narrow
8
+ // `Query` generic like `{ path?: string }` isn't assignable to
9
+ // `Record<string, unknown>` (no index signature), so we widen to
10
+ // `object` and cast internally when reading a key.
9
11
  interface HasQuery {
10
- query: Record<string, unknown>;
12
+ query: object;
13
+ }
14
+
15
+ function readQueryKey(queryObj: object, key: string): unknown {
16
+ return (queryObj as Record<string, unknown>)[key];
11
17
  }
12
18
 
13
19
  /**
@@ -15,7 +21,7 @@ interface HasQuery {
15
21
  * Returns the string value, or "" if missing/non-string.
16
22
  */
17
23
  export function getSessionQuery(req: HasQuery): string {
18
- const raw = req.query.session;
24
+ const raw = readQueryKey(req.query, "session");
19
25
  return typeof raw === "string" ? raw : "";
20
26
  }
21
27
 
@@ -24,6 +30,6 @@ export function getSessionQuery(req: HasQuery): string {
24
30
  * Returns the string value, or undefined if missing/non-string.
25
31
  */
26
32
  export function getOptionalStringQuery(req: HasQuery, key: string): string | undefined {
27
- const raw = req.query[key];
33
+ const raw = readQueryKey(req.query, key);
28
34
  return typeof raw === "string" ? raw : undefined;
29
35
  }
@@ -28,7 +28,7 @@ export function extractClaudeErrorMessage(stdout: string): string | null {
28
28
  if (!isRecord(parsed)) return null;
29
29
  if (parsed.is_error !== true) return null;
30
30
  if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
31
- const joined = parsed.errors.filter((e): e is string => typeof e === "string").join("; ");
31
+ const joined = parsed.errors.filter((err): err is string => typeof err === "string").join("; ");
32
32
  if (joined.length > 0) return joined;
33
33
  }
34
34
  const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
@@ -23,12 +23,12 @@ export function isNonEmptyString(value: unknown): value is string {
23
23
  /** Record whose values are all strings. */
24
24
  export function isStringRecord(value: unknown): value is Record<string, string> {
25
25
  if (!isRecord(value)) return false;
26
- return Object.values(value).every((v) => typeof v === "string");
26
+ return Object.values(value).every((val) => typeof val === "string");
27
27
  }
28
28
 
29
29
  /** String array (every element is a string). */
30
30
  export function isStringArray(value: unknown): value is string[] {
31
- return Array.isArray(value) && value.every((v) => typeof v === "string");
31
+ return Array.isArray(value) && value.every((val) => typeof val === "string");
32
32
  }
33
33
 
34
34
  /** Error-like object with a `code` property (e.g. Node.js fs errors). */