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
@@ -1,5 +1,5 @@
1
1
  import { Router, Request, Response } from "express";
2
- import fs from "fs";
2
+ import { realpathSync } from "fs";
3
3
  import { readdir, stat } from "fs/promises";
4
4
  import { readTextSafe } from "../../utils/files/safe.js";
5
5
  import path from "path";
@@ -26,14 +26,14 @@ interface SessionMeta {
26
26
  origin?: SessionOrigin;
27
27
  }
28
28
 
29
- async function readSessionMeta(__chatDir: string, id: string): Promise<SessionMeta | null> {
29
+ async function readSessionMeta(__chatDir: string, sessionId: string): Promise<SessionMeta | null> {
30
30
  // Try new-style .json meta first
31
- const meta = await readSessionMetaIO(id);
31
+ const meta = await readSessionMetaIO(sessionId);
32
32
  if (meta?.roleId && meta?.startedAt) {
33
33
  return meta as SessionMeta;
34
34
  }
35
35
  // Legacy: read first line of .jsonl
36
- const jsonl = await readSessionJsonl(id);
36
+ const jsonl = await readSessionJsonl(sessionId);
37
37
  if (jsonl) {
38
38
  const first = jsonl.split("\n").find(Boolean);
39
39
  if (first) {
@@ -102,19 +102,19 @@ const WINDOW_MS = env.sessionsListWindowDays * ONE_DAY_MS;
102
102
  export async function loadAllSessions(): Promise<{ summary: SessionSummary; changeMs: number }[]> {
103
103
  const chatDir = WORKSPACE_PATHS.chat;
104
104
  const manifest = await readManifest(workspacePath);
105
- const indexById = new Map<string, ChatIndexEntry>(manifest.entries.map((e) => [e.id, e]));
105
+ const indexById = new Map<string, ChatIndexEntry>(manifest.entries.map((entry) => [entry.id, entry]));
106
106
  const cutoff = WINDOW_MS > 0 ? Date.now() - WINDOW_MS : 0;
107
107
 
108
- const files = (await readdir(chatDir)).filter((f) => f.endsWith(".jsonl"));
108
+ const files = (await readdir(chatDir)).filter((fileName) => fileName.endsWith(".jsonl"));
109
109
  const rows = await Promise.all(
110
110
  files.map(async (file) => {
111
- const id = file.replace(".jsonl", "");
111
+ const sessionId = file.replace(".jsonl", "");
112
112
  try {
113
113
  // stat only — no readFile on .jsonl content
114
- const fileStat = await stat(sessionJsonlAbsPath(id));
114
+ const fileStat = await stat(sessionJsonlAbsPath(sessionId));
115
115
  if (cutoff > 0 && fileStat.mtimeMs < cutoff) return null;
116
116
 
117
- const meta = await readSessionMeta(chatDir, id);
117
+ const meta = await readSessionMeta(chatDir, sessionId);
118
118
  if (!meta) return null;
119
119
 
120
120
  // The meta sidecar bumps its mtime on hasUnread / origin
@@ -122,20 +122,20 @@ export async function loadAllSessions(): Promise<{ summary: SessionSummary; chan
122
122
  // pick up drains of background generations (which only touch
123
123
  // meta, not the jsonl). Missing stat (brand-new session
124
124
  // before its first meta write) contributes 0.
125
- const metaMtimeMs = await stat(sessionMetaAbsPath(id))
126
- .then((s) => s.mtimeMs)
125
+ const metaMtimeMs = await stat(sessionMetaAbsPath(sessionId))
126
+ .then((stats) => stats.mtimeMs)
127
127
  .catch(() => 0);
128
128
 
129
- const indexEntry = indexById.get(id);
129
+ const indexEntry = indexById.get(sessionId);
130
130
  // Prefer AI title → meta.firstUserMessage → empty.
131
131
  // `summary` and `keywords` are spread conditionally
132
132
  // to respect the server tsconfig's
133
133
  // exactOptionalPropertyTypes.
134
134
  const preview = indexEntry?.title ?? meta.firstUserMessage ?? "";
135
135
 
136
- const live = getSession(id);
136
+ const live = getSession(sessionId);
137
137
  const summary: SessionSummary = {
138
- id,
138
+ id: sessionId,
139
139
  roleId: meta.roleId,
140
140
  startedAt: meta.startedAt,
141
141
  updatedAt: new Date(fileStat.mtimeMs).toISOString(),
@@ -161,7 +161,7 @@ export async function loadAllSessions(): Promise<{ summary: SessionSummary; chan
161
161
  }
162
162
  }),
163
163
  );
164
- return rows.filter((r): r is { summary: SessionSummary; changeMs: number } => r !== null);
164
+ return rows.filter((row): row is { summary: SessionSummary; changeMs: number } => row !== null);
165
165
  }
166
166
 
167
167
  router.get(API_ROUTES.sessions.list, async (req: Request<object, SessionsResponse, object, SessionsQuery>, res: Response<SessionsResponse>) => {
@@ -173,15 +173,15 @@ router.get(API_ROUTES.sessions.list, async (req: Request<object, SessionsRespons
173
173
  // of whether it's in the diff. Echoing the same cursor back on an
174
174
  // empty diff (nothing changed since `?since=`) is fine; the
175
175
  // client no-ops.
176
- const maxChangeMs = rows.reduce((acc, r) => Math.max(acc, r.changeMs), 0);
176
+ const maxChangeMs = rows.reduce((acc, row) => Math.max(acc, row.changeMs), 0);
177
177
 
178
- const filtered = sinceMs > 0 ? rows.filter((r) => r.changeMs > sinceMs) : rows;
178
+ const filtered = sinceMs > 0 ? rows.filter((row) => row.changeMs > sinceMs) : rows;
179
179
 
180
- const sessions = filtered.map((r) => r.summary);
181
- sessions.sort((a, b) => {
182
- const byUpdated = new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
180
+ const sessions = filtered.map((row) => row.summary);
181
+ sessions.sort((leftSession, rightSession) => {
182
+ const byUpdated = new Date(rightSession.updatedAt).getTime() - new Date(leftSession.updatedAt).getTime();
183
183
  if (byUpdated !== 0) return byUpdated;
184
- return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
184
+ return new Date(rightSession.startedAt).getTime() - new Date(leftSession.startedAt).getTime();
185
185
  });
186
186
 
187
187
  res.json({
@@ -207,13 +207,13 @@ interface SessionErrorResponse {
207
207
  }
208
208
 
209
209
  router.get(API_ROUTES.sessions.detail, async (req: Request<SessionIdParams>, res: Response<unknown[] | SessionErrorResponse>) => {
210
- const { id } = req.params;
210
+ const { id: sessionId } = req.params;
211
211
  const chatDir = WORKSPACE_PATHS.chat;
212
212
  try {
213
- const meta = await readSessionMeta(chatDir, id);
214
- const content = await readSessionJsonl(id);
213
+ const meta = await readSessionMeta(chatDir, sessionId);
214
+ const content = await readSessionJsonl(sessionId);
215
215
  if (!content) {
216
- notFound(res, `Session ${id} not found`);
216
+ notFound(res, `Session ${sessionId} not found`);
217
217
  return;
218
218
  }
219
219
  const entries = (
@@ -242,7 +242,7 @@ router.get(API_ROUTES.sessions.detail, async (req: Request<SessionIdParams>, res
242
242
  const storiesDir = path.resolve(WORKSPACE_PATHS.stories);
243
243
  let storiesReal: string;
244
244
  try {
245
- storiesReal = fs.realpathSync(storiesDir);
245
+ storiesReal = realpathSync(storiesDir);
246
246
  } catch {
247
247
  return entry;
248
248
  }
@@ -21,8 +21,8 @@ const CURSOR_PREFIX = "v1:";
21
21
  * fall back to when an incoming cursor is malformed.
22
22
  */
23
23
  export function encodeCursor(changeMs: number): string {
24
- const ms = Number.isFinite(changeMs) && changeMs > 0 ? Math.floor(changeMs) : 0;
25
- return `${CURSOR_PREFIX}${ms}`;
24
+ const floored = Number.isFinite(changeMs) && changeMs > 0 ? Math.floor(changeMs) : 0;
25
+ return `${CURSOR_PREFIX}${floored}`;
26
26
  }
27
27
 
28
28
  /**
@@ -36,8 +36,8 @@ export function encodeCursor(changeMs: number): string {
36
36
  export function parseCursor(raw: unknown): number {
37
37
  if (typeof raw !== "string") return 0;
38
38
  if (!raw.startsWith(CURSOR_PREFIX)) return 0;
39
- const n = Number(raw.slice(CURSOR_PREFIX.length));
40
- return Number.isFinite(n) && n > 0 ? n : 0;
39
+ const parsed = Number(raw.slice(CURSOR_PREFIX.length));
40
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
41
41
  }
42
42
 
43
43
  /**
@@ -55,17 +55,17 @@ interface DeleteSkillResponse {
55
55
  router.get(API_ROUTES.skills.list, async (_req: Request, res: Response<SkillsListResponse>) => {
56
56
  const skills = await discoverSkills({ workspaceRoot: workspacePath });
57
57
  res.json({
58
- skills: skills.map((s) => ({
59
- name: s.name,
60
- description: s.description,
61
- source: s.source,
58
+ skills: skills.map((skill) => ({
59
+ name: skill.name,
60
+ description: skill.description,
61
+ source: skill.source,
62
62
  })),
63
63
  });
64
64
  });
65
65
 
66
66
  router.get(API_ROUTES.skills.detail, async (req: Request<{ name: string }>, res: Response<SkillDetailResponse | ErrorResponse>) => {
67
67
  const skills = await discoverSkills({ workspaceRoot: workspacePath });
68
- const skill = skills.find((s) => s.name === req.params.name);
68
+ const skill = skills.find((candidate) => candidate.name === req.params.name);
69
69
  if (!skill) {
70
70
  notFound(res, `skill not found: ${req.params.name}`);
71
71
  return;
@@ -312,7 +312,7 @@ async function handleRegister(body: ManageSourceBody, res: Response<ManageSource
312
312
  async function handleRemove(body: ManageSourceBody, res: Response<ManageSourceSuccess | ErrorResponse>): Promise<void> {
313
313
  const slug = typeof body.slug === "string" ? body.slug.trim() : "";
314
314
  if (!isValidSlug(slug)) {
315
- res.status(400).json({ error: "slug is required and must be a valid slug" });
315
+ badRequest(res, "slug is required and must be a valid slug");
316
316
  return;
317
317
  }
318
318
  const removed = await deleteSource(workspacePath, slug);
@@ -525,8 +525,8 @@ async function resolveCategories(parsed: ParsedRegisterBody): Promise<{ categori
525
525
 
526
526
  function isHttpUrl(raw: string): boolean {
527
527
  try {
528
- const u = new URL(raw);
529
- return u.protocol === "http:" || u.protocol === "https:";
528
+ const url = new URL(raw);
529
+ return url.protocol === "http:" || url.protocol === "https:";
530
530
  } catch {
531
531
  return false;
532
532
  }
@@ -79,8 +79,8 @@ function uniqueId(base: string, existingIds: ReadonlySet<string>): string {
79
79
 
80
80
  // ── Validation helpers ────────────────────────────────────────────
81
81
 
82
- function findColumn(columns: StatusColumn[], id: string): StatusColumn | undefined {
83
- return columns.find((column) => column.id === id);
82
+ function findColumn(columns: StatusColumn[], columnId: string): StatusColumn | undefined {
83
+ return columns.find((column) => column.id === columnId);
84
84
  }
85
85
 
86
86
  function ensureColumnsValid(columns: StatusColumn[]): StatusColumn[] {
@@ -161,11 +161,11 @@ export function defaultStatusId(columns: StatusColumn[]): string {
161
161
  // to sync at that point.
162
162
  export function resyncDoneMembership(items: TodoItem[], newDoneId: string): { items: TodoItem[]; changed: boolean } {
163
163
  let changed = false;
164
- const next = items.map((it): TodoItem => {
165
- const shouldBeDone = it.status === newDoneId;
166
- if (it.completed === shouldBeDone) return it;
164
+ const next = items.map((item): TodoItem => {
165
+ const shouldBeDone = item.status === newDoneId;
166
+ if (item.completed === shouldBeDone) return item;
167
167
  changed = true;
168
- return { ...it, completed: shouldBeDone };
168
+ return { ...item, completed: shouldBeDone };
169
169
  });
170
170
  return { items: next, changed };
171
171
  }
@@ -178,7 +178,7 @@ export function resyncDoneMembership(items: TodoItem[], newDoneId: string): { it
178
178
  function rebuildColumnOrder(items: TodoItem[], columnId: string): TodoItem[] {
179
179
  const inColumn = items.filter((item) => item.status === columnId).sort((left, right) => (left.order ?? 0) - (right.order ?? 0));
180
180
  const newOrders = new Map<string, number>();
181
- inColumn.forEach((item, i) => newOrders.set(item.id, (i + 1) * ORDER_STEP));
181
+ inColumn.forEach((item, index) => newOrders.set(item.id, (index + 1) * ORDER_STEP));
182
182
  return items.map((item): TodoItem => {
183
183
  const newOrder = newOrders.get(item.id);
184
184
  if (newOrder === undefined) return item;
@@ -200,23 +200,23 @@ export function handleAddColumn(columns: StatusColumn[], items: TodoItem[], inpu
200
200
  return { kind: "error", status: 400, error: "label required" };
201
201
  }
202
202
  const baseId = slugify(input.label);
203
- const id = uniqueId(baseId, new Set(columns.map((column) => column.id)));
204
- const col: StatusColumn = { id, label: input.label.trim() };
205
- if (input.isDone === true) col.isDone = true;
203
+ const columnId = uniqueId(baseId, new Set(columns.map((column) => column.id)));
204
+ const columnToAdd: StatusColumn = { id: columnId, label: input.label.trim() };
205
+ if (input.isDone === true) columnToAdd.isDone = true;
206
206
  // If the new column is flagged done, demote any existing done
207
207
  // columns (only one is allowed at a time) and resync items so the
208
208
  // old done column's items are no longer marked completed. The new
209
209
  // column itself is empty so there's nothing on its side to sync.
210
210
  if (input.isDone === true) {
211
- const nextColumns = [...columns.map((column) => ({ ...column, isDone: false })), col];
212
- const { items: nextItems, changed } = resyncDoneMembership(items, id);
211
+ const nextColumns = [...columns.map((column) => ({ ...column, isDone: false })), columnToAdd];
212
+ const { items: nextItems, changed } = resyncDoneMembership(items, columnId);
213
213
  return {
214
214
  kind: "success",
215
215
  columns: nextColumns,
216
216
  ...(changed ? { items: nextItems } : {}),
217
217
  };
218
218
  }
219
- return { kind: "success", columns: [...columns, col] };
219
+ return { kind: "success", columns: [...columns, columnToAdd] };
220
220
  }
221
221
 
222
222
  export interface PatchColumnInput {
@@ -224,28 +224,28 @@ export interface PatchColumnInput {
224
224
  isDone?: boolean;
225
225
  }
226
226
 
227
- export function handlePatchColumn(columns: StatusColumn[], id: string, input: PatchColumnInput, items: TodoItem[]): ColumnsActionResult {
228
- const target = findColumn(columns, id);
227
+ export function handlePatchColumn(columns: StatusColumn[], columnId: string, input: PatchColumnInput, items: TodoItem[]): ColumnsActionResult {
228
+ const target = findColumn(columns, columnId);
229
229
  if (!target) {
230
- return { kind: "error", status: 404, error: `column not found: ${id}` };
230
+ return { kind: "error", status: 404, error: `column not found: ${columnId}` };
231
231
  }
232
232
  const patched: StatusColumn = { id: target.id, label: target.label };
233
233
  if (target.isDone) patched.isDone = true;
234
234
  if (typeof input.label === "string" && input.label.trim().length > 0) {
235
235
  patched.label = input.label.trim();
236
236
  }
237
- let nextColumns = columns.map((column) => (column.id === id ? patched : column));
237
+ let nextColumns = columns.map((column) => (column.id === columnId ? patched : column));
238
238
  // Toggling done flag is non-trivial: only one column may be done.
239
239
  let itemsChanged = false;
240
240
  let nextItems = items;
241
241
  if (input.isDone === true && !target.isDone) {
242
242
  // Promote this column to done; demote everyone else.
243
- nextColumns = nextColumns.map((column) => (column.id === id ? { ...column, isDone: true } : { id: column.id, label: column.label }));
243
+ nextColumns = nextColumns.map((column) => (column.id === columnId ? { ...column, isDone: true } : { id: column.id, label: column.label }));
244
244
  // Resync `completed` across all items: the new done column's
245
245
  // items become true, the old done column's items become false.
246
246
  // Doing this with the helper rather than a one-sided pass means
247
247
  // both ends of the swap stay consistent.
248
- const synced = resyncDoneMembership(items, id);
248
+ const synced = resyncDoneMembership(items, columnId);
249
249
  nextItems = synced.items;
250
250
  itemsChanged = synced.changed;
251
251
  } else if (input.isDone === false && target.isDone) {
@@ -263,7 +263,7 @@ export function handlePatchColumn(columns: StatusColumn[], id: string, input: Pa
263
263
  };
264
264
  }
265
265
 
266
- export function handleDeleteColumn(columns: StatusColumn[], id: string, items: TodoItem[]): ColumnsActionResult {
266
+ export function handleDeleteColumn(columns: StatusColumn[], columnId: string, items: TodoItem[]): ColumnsActionResult {
267
267
  if (columns.length <= 1) {
268
268
  return {
269
269
  kind: "error",
@@ -271,11 +271,11 @@ export function handleDeleteColumn(columns: StatusColumn[], id: string, items: T
271
271
  error: "cannot delete the last remaining column",
272
272
  };
273
273
  }
274
- const target = findColumn(columns, id);
274
+ const target = findColumn(columns, columnId);
275
275
  if (!target) {
276
- return { kind: "error", status: 404, error: `column not found: ${id}` };
276
+ return { kind: "error", status: 404, error: `column not found: ${columnId}` };
277
277
  }
278
- const remaining = columns.filter((column) => column.id !== id);
278
+ const remaining = columns.filter((column) => column.id !== columnId);
279
279
  // If we just removed the done column, promote the new last column.
280
280
  let nextColumns = remaining;
281
281
  if (target.isDone) {
@@ -286,10 +286,10 @@ export function handleDeleteColumn(columns: StatusColumn[], id: string, items: T
286
286
  // deleted column was done; otherwise to the new default open column.
287
287
  const refugeId = target.isDone ? newDoneId : defaultStatusId(nextColumns);
288
288
  let itemsChanged = false;
289
- let nextItems = items.map((it): TodoItem => {
290
- if (it.status !== id) return it;
289
+ let nextItems = items.map((item): TodoItem => {
290
+ if (item.status !== columnId) return item;
291
291
  itemsChanged = true;
292
- return { ...it, status: refugeId };
292
+ return { ...item, status: refugeId };
293
293
  });
294
294
  if (itemsChanged) {
295
295
  // The refuge column might have already had items in it; the ones
@@ -331,17 +331,17 @@ export function handleReorderColumns(columns: StatusColumn[], ids: string[]): Co
331
331
  }
332
332
  const known = new Set(columns.map((column) => column.id));
333
333
  const seen = new Set<string>();
334
- for (const id of ids) {
335
- if (!known.has(id) || seen.has(id)) {
334
+ for (const columnId of ids) {
335
+ if (!known.has(columnId) || seen.has(columnId)) {
336
336
  return {
337
337
  kind: "error",
338
338
  status: 400,
339
339
  error: "ids must contain every existing column id exactly once",
340
340
  };
341
341
  }
342
- seen.add(id);
342
+ seen.add(columnId);
343
343
  }
344
344
  const byId = new Map(columns.map((column) => [column.id, column]));
345
- const next = ids.map((id) => byId.get(id)!);
345
+ const next = ids.map((columnId) => byId.get(columnId)!);
346
346
  return { kind: "success", columns: next };
347
347
  }
@@ -242,7 +242,7 @@ export function handleRemoveLabel(items: TodoItem[], input: TodosActionInput): T
242
242
 
243
243
  export function handleListLabels(items: TodoItem[]): TodosActionResult {
244
244
  const inventory = listLabelsWithCount(items);
245
- const summary = inventory.map((l) => `${l.label} (${l.count})`).join(", ");
245
+ const summary = inventory.map((entry) => `${entry.label} (${entry.count})`).join(", ");
246
246
  const message = inventory.length === 0 ? "No labels in use" : `${inventory.length} label(s) in use: ${summary}`;
247
247
  return {
248
248
  kind: "success",
@@ -281,10 +281,10 @@ function applyCompletedPatch(updated: TodoItem, items: TodoItem[], columns: Stat
281
281
  }
282
282
  }
283
283
 
284
- export function handlePatch(items: TodoItem[], columns: StatusColumn[], id: string, input: PatchInput): ItemsActionResult {
285
- const target = items.find((i) => i.id === id);
284
+ export function handlePatch(items: TodoItem[], columns: StatusColumn[], itemId: string, input: PatchInput): ItemsActionResult {
285
+ const target = items.find((item) => item.id === itemId);
286
286
  if (!target) {
287
- return { kind: "error", status: 404, error: `item not found: ${id}` };
287
+ return { kind: "error", status: 404, error: `item not found: ${itemId}` };
288
288
  }
289
289
  const updated: TodoItem = { ...target };
290
290
 
@@ -305,7 +305,7 @@ export function handlePatch(items: TodoItem[], columns: StatusColumn[], id: stri
305
305
  if (err) return err;
306
306
  }
307
307
 
308
- const next = items.map((item) => (item.id === id ? updated : item));
308
+ const next = items.map((item) => (item.id === itemId ? updated : item));
309
309
  return { kind: "success", items: next, item: updated };
310
310
  }
311
311
 
@@ -323,10 +323,10 @@ export interface MoveInput {
323
323
  position?: number;
324
324
  }
325
325
 
326
- export function handleMove(items: TodoItem[], columns: StatusColumn[], id: string, input: MoveInput): ItemsActionResult {
327
- const target = items.find((i) => i.id === id);
326
+ export function handleMove(items: TodoItem[], columns: StatusColumn[], itemId: string, input: MoveInput): ItemsActionResult {
327
+ const target = items.find((item) => item.id === itemId);
328
328
  if (!target) {
329
- return { kind: "error", status: 404, error: `item not found: ${id}` };
329
+ return { kind: "error", status: 404, error: `item not found: ${itemId}` };
330
330
  }
331
331
  const validStatusIds = new Set(columns.map((column) => column.id));
332
332
  const newStatus = input.status ?? target.status ?? defaultStatusId(columns);
@@ -345,7 +345,7 @@ export function handleMove(items: TodoItem[], columns: StatusColumn[], id: strin
345
345
  };
346
346
  // Re-collect the items in the target column with the moving item
347
347
  // pulled out, then splice item back in at `position`.
348
- const others = items.filter((item) => item.id !== id && item.status === newStatus).sort((left, right) => (left.order ?? 0) - (right.order ?? 0));
348
+ const others = items.filter((item) => item.id !== itemId && item.status === newStatus).sort((left, right) => (left.order ?? 0) - (right.order ?? 0));
349
349
  const insertAt = clampPosition(input.position, others.length);
350
350
  const reordered = [...others];
351
351
  reordered.splice(insertAt, 0, updatedSelf);
@@ -354,7 +354,7 @@ export function handleMove(items: TodoItem[], columns: StatusColumn[], id: strin
354
354
  reordered.forEach((item, i) => reorderedById.set(item.id, (i + 1) * ORDER_STEP));
355
355
  const nextItems = items.map((item): TodoItem => {
356
356
  const newOrder = reorderedById.get(item.id);
357
- if (item.id === id) {
357
+ if (item.id === itemId) {
358
358
  const out: TodoItem = {
359
359
  ...updatedSelf,
360
360
  order: newOrder ?? updatedSelf.order ?? ORDER_STEP,
@@ -364,7 +364,7 @@ export function handleMove(items: TodoItem[], columns: StatusColumn[], id: strin
364
364
  if (newOrder !== undefined) return { ...item, order: newOrder };
365
365
  return item;
366
366
  });
367
- const finalSelf = nextItems.find((item) => item.id === id)!;
367
+ const finalSelf = nextItems.find((item) => item.id === itemId)!;
368
368
  return { kind: "success", items: nextItems, item: finalSelf };
369
369
  }
370
370
 
@@ -377,10 +377,10 @@ function clampPosition(raw: number | undefined, max: number): number {
377
377
 
378
378
  // ── Delete ────────────────────────────────────────────────────────
379
379
 
380
- export function handleDeleteItem(items: TodoItem[], id: string): ItemsActionResult {
381
- const target = items.find((i) => i.id === id);
380
+ export function handleDeleteItem(items: TodoItem[], itemId: string): ItemsActionResult {
381
+ const target = items.find((item) => item.id === itemId);
382
382
  if (!target) {
383
- return { kind: "error", status: 404, error: `item not found: ${id}` };
383
+ return { kind: "error", status: 404, error: `item not found: ${itemId}` };
384
384
  }
385
- return { kind: "success", items: items.filter((item) => item.id !== id) };
385
+ return { kind: "success", items: items.filter((item) => item.id !== itemId) };
386
386
  }
@@ -4,6 +4,7 @@ import { WORKSPACE_PATHS } from "../../workspace/paths.js";
4
4
  import { readTextSafeSync, readTextSafe } from "../../utils/files/safe.js";
5
5
  import { getPageIndex } from "./wiki/pageIndex.js";
6
6
  import { badRequest } from "../../utils/httpError.js";
7
+ import { getOptionalStringQuery } from "../../utils/request.js";
7
8
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
8
9
 
9
10
  const router = Router();
@@ -46,7 +47,7 @@ function parseTableRow(trimmed: string): WikiPageEntry | null {
46
47
  const cols = trimmed
47
48
  .split("|")
48
49
  .slice(1, -1)
49
- .map((c) => c.trim().replace(/^`|`$/g, ""));
50
+ .map((column) => column.trim().replace(/^`|`$/g, ""));
50
51
  if (cols.length < 2) return null;
51
52
  const slug = cols[0];
52
53
  const title = cols[1] || slug;
@@ -70,11 +71,11 @@ export function extractSlugFromBulletHref(rawHref: string): string {
70
71
  }
71
72
 
72
73
  function parseBulletLinkRow(trimmed: string): WikiPageEntry | null {
73
- const m = BULLET_LINK_PATTERN.exec(trimmed);
74
- if (!m) return null;
75
- const title = m[1].trim();
76
- const href = m[2] ?? "";
77
- const desc = m[3]?.trim() ?? "";
74
+ const match = BULLET_LINK_PATTERN.exec(trimmed);
75
+ if (!match) return null;
76
+ const title = match[1].trim();
77
+ const href = match[2] ?? "";
78
+ const desc = match[3]?.trim() ?? "";
78
79
  // Prefer the slug embedded in the href so non-ASCII titles keep
79
80
  // a navigable slug. Fall back to slugifying the title only when
80
81
  // the href has no recognisable slug (rare — usually means the
@@ -84,10 +85,10 @@ function parseBulletLinkRow(trimmed: string): WikiPageEntry | null {
84
85
  }
85
86
 
86
87
  function parseBulletWikiLinkRow(trimmed: string): WikiPageEntry | null {
87
- const m = BULLET_WIKI_LINK_PATTERN.exec(trimmed);
88
- if (!m) return null;
89
- const title = m[1].trim();
90
- const desc = m[2]?.trim() ?? "";
88
+ const match = BULLET_WIKI_LINK_PATTERN.exec(trimmed);
89
+ if (!match) return null;
90
+ const title = match[1].trim();
91
+ const desc = match[2]?.trim() ?? "";
91
92
  return { title, slug: wikiSlugify(title), description: desc };
92
93
  }
93
94
 
@@ -132,21 +133,34 @@ async function resolvePagePath(pageName: string): Promise<string | null> {
132
133
 
133
134
  const slug = wikiSlugify(pageName);
134
135
 
135
- const exact = slugs.get(slug);
136
- if (exact) return path.join(dir, exact);
136
+ if (slug.length > 0) {
137
+ const exact = slugs.get(slug);
138
+ if (exact) return path.join(dir, exact);
137
139
 
138
- // Fuzzy: same `includes` semantics as the old sync path — iterate
139
- // the index's keys, no filesystem access.
140
- for (const [key, file] of slugs) {
141
- if (slug.includes(key) || key.includes(slug)) {
142
- return path.join(dir, file);
140
+ // Fuzzy: same `includes` semantics as the old sync path — iterate
141
+ // the index's keys, no filesystem access.
142
+ for (const [key, file] of slugs) {
143
+ if (slug.includes(key) || key.includes(slug)) {
144
+ return path.join(dir, file);
145
+ }
143
146
  }
144
147
  }
148
+
149
+ // Non-ASCII page names (e.g. Japanese [[wiki links]]) produce empty
150
+ // slugs after slugification. Fall back to matching by title in the
151
+ // wiki index so the link resolves to its page file.
152
+ const indexContent = readFileOrEmpty(indexFile());
153
+ const entries = parseIndexEntries(indexContent);
154
+ const titleMatch = entries.find((entry) => entry.title === pageName);
155
+ if (titleMatch && slugs.has(titleMatch.slug)) {
156
+ return path.join(dir, slugs.get(titleMatch.slug)!);
157
+ }
158
+
145
159
  return null;
146
160
  }
147
161
 
148
162
  router.get(API_ROUTES.wiki.base, async (req: Request, res: Response<WikiResponse | ErrorResponse>) => {
149
- const slug = typeof req.query.slug === "string" ? req.query.slug : undefined;
163
+ const slug = getOptionalStringQuery(req, "slug");
150
164
  if (slug) {
151
165
  const filePath = await resolvePagePath(slug);
152
166
  const content = filePath ? readFileOrEmpty(filePath) : "";
@@ -276,7 +290,7 @@ export function findMissingFiles(pageEntries: readonly WikiPageEntry[], fileSlug
276
290
 
277
291
  export function findBrokenLinksInPage(fileName: string, content: string, fileSlugs: ReadonlySet<string>): string[] {
278
292
  const issues: string[] = [];
279
- const wikiLinks = [...content.matchAll(WIKI_LINK_PATTERN)].map((m) => m[1]);
293
+ const wikiLinks = [...content.matchAll(WIKI_LINK_PATTERN)].map((match) => match[1]);
280
294
  for (const link of wikiLinks) {
281
295
  const linkSlug = wikiSlugify(link);
282
296
  if (!fileSlugs.has(linkSlug)) {
@@ -302,7 +316,7 @@ async function collectLintIssues(): Promise<string[]> {
302
316
  }
303
317
  const indexContent = readFileOrEmpty(indexFile());
304
318
  const pageEntries = parseIndexEntries(indexContent);
305
- const indexedSlugs = new Set(pageEntries.map((e) => e.slug));
319
+ const indexedSlugs = new Set(pageEntries.map((entry) => entry.slug));
306
320
  const pageFiles = [...slugs.values()];
307
321
  const fileSlugs = new Set(slugs.keys());
308
322
 
@@ -312,8 +326,8 @@ async function collectLintIssues(): Promise<string[]> {
312
326
  // Parallel read: N small markdown files, ~50 KB each. Bounded by
313
327
  // the number of wiki pages, not by CPU.
314
328
  const contents = await Promise.all(
315
- pageFiles.map(async (f) => {
316
- const content = await readTextSafe(path.join(dir, f));
329
+ pageFiles.map(async (fileName) => {
330
+ const content = await readTextSafe(path.join(dir, fileName));
317
331
  return content ?? "";
318
332
  }),
319
333
  );
@@ -59,6 +59,6 @@ export function buildSandboxStatus(params: BuildSandboxStatusParams): SandboxSta
59
59
 
60
60
  return {
61
61
  sshAgent,
62
- mounts: parsed.resolved.map((m) => m.name),
62
+ mounts: parsed.resolved.map((mount) => mount.name),
63
63
  };
64
64
  }
@@ -32,8 +32,8 @@ export interface NotificationDeps {
32
32
 
33
33
  let deps: NotificationDeps | null = null;
34
34
 
35
- export function initNotifications(d: NotificationDeps): void {
36
- deps = d;
35
+ export function initNotifications(injected: NotificationDeps): void {
36
+ deps = injected;
37
37
  }
38
38
 
39
39
  // ── In-memory store ─────────────────────────────────────────────
@@ -97,10 +97,10 @@ export function publishNotification(opts: PublishNotificationOpts): void {
97
97
  }
98
98
  }
99
99
 
100
- function formatBridgeMessage(p: NotificationPayload): string {
101
- const icon = p.kind === NOTIFICATION_KINDS.agent ? "\u2705" : "\u{1F514}";
102
- const parts = [icon, p.title];
103
- if (p.body) parts.push(p.body);
100
+ function formatBridgeMessage(payload: NotificationPayload): string {
101
+ const icon = payload.kind === NOTIFICATION_KINDS.agent ? "\u2705" : "\u{1F514}";
102
+ const parts = [icon, payload.title];
103
+ if (payload.body) parts.push(payload.body);
104
104
  return parts.join(" ");
105
105
  }
106
106
 
@@ -13,7 +13,7 @@ export interface IPubSub {
13
13
  // itself, which is why we switched off raw ws.
14
14
 
15
15
  export function createPubSub(server: http.Server): IPubSub {
16
- const io = new IOServer(server, {
16
+ const ioServer = new IOServer(server, {
17
17
  path: "/ws/pubsub",
18
18
  // Server binds to 127.0.0.1 only, so CORS is moot — but
19
19
  // socket.io defaults to rejecting cross-origin upgrade
@@ -28,7 +28,7 @@ export function createPubSub(server: http.Server): IPubSub {
28
28
  transports: ["websocket"],
29
29
  });
30
30
 
31
- io.on("connection", (socket) => {
31
+ ioServer.on("connection", (socket) => {
32
32
  socket.on("subscribe", (channel: unknown) => {
33
33
  if (typeof channel === "string") socket.join(channel);
34
34
  });
@@ -39,7 +39,7 @@ export function createPubSub(server: http.Server): IPubSub {
39
39
 
40
40
  return {
41
41
  publish(channel: string, data: unknown): void {
42
- io.to(channel).emit("data", { channel, data });
42
+ ioServer.to(channel).emit("data", { channel, data });
43
43
  },
44
44
  };
45
45
  }