mulmoclaude 0.1.2 → 0.3.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 (120) hide show
  1. package/bin/mulmoclaude.js +1 -1
  2. package/client/assets/{index-KNLBjwuh.css → index-Bm70FDU2.css} +1 -1
  3. package/client/assets/{index-D8rhwXLq.js → index-eHWB79u5.js} +3 -3
  4. package/client/index.html +2 -2
  5. package/package.json +1 -1
  6. package/server/agent/config.ts +12 -12
  7. package/server/agent/mcp-server.ts +19 -19
  8. package/server/agent/mcp-tools/x.ts +5 -5
  9. package/server/agent/prompt.ts +9 -4
  10. package/server/agent/sandboxMounts.ts +7 -7
  11. package/server/agent/stream.ts +4 -4
  12. package/server/api/routes/files.ts +9 -9
  13. package/server/api/routes/scheduler.ts +8 -8
  14. package/server/api/routes/schedulerHandlers.ts +12 -12
  15. package/server/api/routes/schedulerTasks.ts +14 -14
  16. package/server/api/routes/sessions.ts +24 -24
  17. package/server/api/routes/todosColumnsHandlers.ts +30 -30
  18. package/server/api/routes/wiki.ts +14 -14
  19. package/server/events/scheduler-adapter.ts +20 -20
  20. package/server/events/session-store/index.ts +10 -10
  21. package/server/events/task-manager/index.ts +7 -7
  22. package/server/index.ts +19 -19
  23. package/server/utils/date.ts +18 -18
  24. package/server/utils/files/atomic.ts +9 -9
  25. package/server/utils/files/html-io.ts +5 -5
  26. package/server/utils/files/image-store.ts +2 -2
  27. package/server/utils/files/journal-io.ts +2 -2
  28. package/server/utils/files/naming.ts +2 -2
  29. package/server/utils/files/roles-io.ts +10 -10
  30. package/server/utils/files/scheduler-io.ts +5 -5
  31. package/server/utils/files/session-io.ts +35 -35
  32. package/server/utils/files/spreadsheet-store.ts +2 -2
  33. package/server/utils/files/todos-io.ts +9 -9
  34. package/server/utils/files/user-tasks-io.ts +5 -5
  35. package/server/workspace/chat-index/indexer.ts +15 -15
  36. package/server/workspace/custom-dirs.ts +11 -11
  37. package/server/workspace/journal/archivist.ts +35 -35
  38. package/server/workspace/journal/dailyPass.ts +31 -28
  39. package/server/workspace/journal/indexFile.ts +29 -25
  40. package/server/workspace/reference-dirs.ts +18 -18
  41. package/server/workspace/roles.ts +6 -6
  42. package/server/workspace/skills/discovery.ts +4 -4
  43. package/server/workspace/skills/user-tasks.ts +34 -34
  44. package/server/workspace/sources/arxivDiscovery.ts +8 -8
  45. package/server/workspace/sources/classifier.ts +7 -7
  46. package/server/workspace/sources/fetchers/arxiv.ts +7 -7
  47. package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
  48. package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
  49. package/server/workspace/sources/interests.ts +9 -9
  50. package/server/workspace/sources/pipeline/index.ts +6 -6
  51. package/server/workspace/sources/pipeline/plan.ts +5 -5
  52. package/server/workspace/sources/registry.ts +16 -16
  53. package/server/workspace/sources/robots.ts +14 -14
  54. package/server/workspace/sources/sourceState.ts +11 -9
  55. package/server/workspace/tool-trace/index.ts +1 -1
  56. package/server/workspace/tool-trace/writeSearch.ts +26 -16
  57. package/server/workspace/wiki-backlinks/index.ts +8 -8
  58. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
  59. package/src/App.vue +30 -30
  60. package/src/components/ChatInput.vue +7 -7
  61. package/src/components/LockStatusPopup.vue +2 -2
  62. package/src/components/NotificationToast.vue +2 -2
  63. package/src/components/RoleSelector.vue +2 -2
  64. package/src/components/SessionHistoryPanel.vue +6 -6
  65. package/src/components/SettingsMcpTab.vue +7 -7
  66. package/src/components/SettingsModal.vue +3 -3
  67. package/src/components/SettingsReferenceDirsTab.vue +10 -10
  68. package/src/components/SettingsWorkspaceDirsTab.vue +5 -5
  69. package/src/components/SuggestionsPanel.vue +2 -2
  70. package/src/components/todo/TodoAddDialog.vue +2 -2
  71. package/src/components/todo/TodoEditPanel.vue +2 -2
  72. package/src/components/todo/TodoListView.vue +5 -5
  73. package/src/composables/useCanvasViewMode.ts +5 -5
  74. package/src/composables/useClickOutside.ts +2 -2
  75. package/src/composables/useFreshPluginData.ts +3 -3
  76. package/src/composables/useKeyNavigation.ts +11 -11
  77. package/src/composables/useMcpTools.ts +2 -2
  78. package/src/composables/useNotifications.ts +3 -3
  79. package/src/composables/usePdfDownload.ts +4 -4
  80. package/src/composables/usePendingCalls.ts +1 -1
  81. package/src/composables/usePubSub.ts +10 -10
  82. package/src/composables/useRoles.ts +1 -1
  83. package/src/composables/useSandboxStatus.ts +1 -1
  84. package/src/composables/useSessionDerived.ts +3 -3
  85. package/src/composables/useSessionSync.ts +8 -8
  86. package/src/composables/useViewLayout.ts +2 -2
  87. package/src/config/roles.ts +2 -2
  88. package/src/plugins/chart/Preview.vue +4 -4
  89. package/src/plugins/manageSkills/View.vue +3 -3
  90. package/src/plugins/manageSource/Preview.vue +1 -1
  91. package/src/plugins/markdown/View.vue +2 -2
  92. package/src/plugins/presentHtml/helpers.ts +8 -8
  93. package/src/plugins/presentMulmoScript/View.vue +4 -4
  94. package/src/plugins/presentMulmoScript/helpers.ts +1 -1
  95. package/src/plugins/scheduler/Preview.vue +6 -6
  96. package/src/plugins/scheduler/TasksTab.vue +4 -4
  97. package/src/plugins/textResponse/View.vue +2 -2
  98. package/src/plugins/todo/Preview.vue +2 -2
  99. package/src/plugins/todo/View.vue +11 -11
  100. package/src/plugins/todo/composables/useTodos.ts +5 -5
  101. package/src/plugins/wiki/Preview.vue +5 -5
  102. package/src/plugins/wiki/helpers.ts +4 -4
  103. package/src/router/guards.ts +12 -12
  104. package/src/types/session.ts +4 -3
  105. package/src/utils/agent/request.ts +3 -3
  106. package/src/utils/dom/scrollable.ts +2 -2
  107. package/src/utils/files/expandedDirs.ts +1 -1
  108. package/src/utils/files/sortChildren.ts +6 -6
  109. package/src/utils/format/frontmatter.ts +6 -6
  110. package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
  111. package/src/utils/markdown/extractFirstH1.ts +2 -2
  112. package/src/utils/path/relativeLink.ts +15 -15
  113. package/src/utils/role/icon.ts +2 -2
  114. package/src/utils/role/merge.ts +2 -2
  115. package/src/utils/role/plugins.ts +1 -1
  116. package/src/utils/session/sessionFactory.ts +2 -2
  117. package/src/utils/session/sessionHelpers.ts +2 -2
  118. package/src/utils/tools/dedup.ts +4 -4
  119. package/src/utils/tools/result.ts +3 -3
  120. package/src/utils/types.ts +2 -2
@@ -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 = (
@@ -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
  }
@@ -46,7 +46,7 @@ function parseTableRow(trimmed: string): WikiPageEntry | null {
46
46
  const cols = trimmed
47
47
  .split("|")
48
48
  .slice(1, -1)
49
- .map((c) => c.trim().replace(/^`|`$/g, ""));
49
+ .map((column) => column.trim().replace(/^`|`$/g, ""));
50
50
  if (cols.length < 2) return null;
51
51
  const slug = cols[0];
52
52
  const title = cols[1] || slug;
@@ -70,11 +70,11 @@ export function extractSlugFromBulletHref(rawHref: string): string {
70
70
  }
71
71
 
72
72
  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() ?? "";
73
+ const match = BULLET_LINK_PATTERN.exec(trimmed);
74
+ if (!match) return null;
75
+ const title = match[1].trim();
76
+ const href = match[2] ?? "";
77
+ const desc = match[3]?.trim() ?? "";
78
78
  // Prefer the slug embedded in the href so non-ASCII titles keep
79
79
  // a navigable slug. Fall back to slugifying the title only when
80
80
  // the href has no recognisable slug (rare — usually means the
@@ -84,10 +84,10 @@ function parseBulletLinkRow(trimmed: string): WikiPageEntry | null {
84
84
  }
85
85
 
86
86
  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() ?? "";
87
+ const match = BULLET_WIKI_LINK_PATTERN.exec(trimmed);
88
+ if (!match) return null;
89
+ const title = match[1].trim();
90
+ const desc = match[2]?.trim() ?? "";
91
91
  return { title, slug: wikiSlugify(title), description: desc };
92
92
  }
93
93
 
@@ -276,7 +276,7 @@ export function findMissingFiles(pageEntries: readonly WikiPageEntry[], fileSlug
276
276
 
277
277
  export function findBrokenLinksInPage(fileName: string, content: string, fileSlugs: ReadonlySet<string>): string[] {
278
278
  const issues: string[] = [];
279
- const wikiLinks = [...content.matchAll(WIKI_LINK_PATTERN)].map((m) => m[1]);
279
+ const wikiLinks = [...content.matchAll(WIKI_LINK_PATTERN)].map((match) => match[1]);
280
280
  for (const link of wikiLinks) {
281
281
  const linkSlug = wikiSlugify(link);
282
282
  if (!fileSlugs.has(linkSlug)) {
@@ -302,7 +302,7 @@ async function collectLintIssues(): Promise<string[]> {
302
302
  }
303
303
  const indexContent = readFileOrEmpty(indexFile());
304
304
  const pageEntries = parseIndexEntries(indexContent);
305
- const indexedSlugs = new Set(pageEntries.map((e) => e.slug));
305
+ const indexedSlugs = new Set(pageEntries.map((entry) => entry.slug));
306
306
  const pageFiles = [...slugs.values()];
307
307
  const fileSlugs = new Set(slugs.keys());
308
308
 
@@ -312,8 +312,8 @@ async function collectLintIssues(): Promise<string[]> {
312
312
  // Parallel read: N small markdown files, ~50 KB each. Bounded by
313
313
  // the number of wiki pages, not by CPU.
314
314
  const contents = await Promise.all(
315
- pageFiles.map(async (f) => {
316
- const content = await readTextSafe(path.join(dir, f));
315
+ pageFiles.map(async (fileName) => {
316
+ const content = await readTextSafe(path.join(dir, fileName));
317
317
  return content ?? "";
318
318
  }),
319
319
  );
@@ -52,16 +52,16 @@ function logsDir(root = workspacePath): string {
52
52
  // ── I/O deps (real filesystem) ───────────────────────────────────
53
53
 
54
54
  const stateDeps: StateDeps = {
55
- readFile: (p: string) => readFile(p, "utf-8"),
56
- writeFileAtomic: (p: string, content: string) => writeFileAtomic(p, content),
55
+ readFile: (filePath: string) => readFile(filePath, "utf-8"),
56
+ writeFileAtomic: (filePath: string, content: string) => writeFileAtomic(filePath, content),
57
57
  exists: existsSync,
58
58
  };
59
59
 
60
60
  const logDeps: LogDeps = {
61
- appendFile: (p: string, content: string) => appendFile(p, content),
62
- readFile: (p: string) => readFile(p, "utf-8"),
61
+ appendFile: (filePath: string, content: string) => appendFile(filePath, content),
62
+ readFile: (filePath: string) => readFile(filePath, "utf-8"),
63
63
  exists: existsSync,
64
- ensureDir: (p: string) => mkdir(p, { recursive: true }).then(() => {}),
64
+ ensureDir: (directoryPath: string) => mkdir(directoryPath, { recursive: true }).then(() => {}),
65
65
  };
66
66
 
67
67
  // ── System task registry ─────────────────────────────────────────
@@ -95,11 +95,11 @@ export async function initScheduler(taskManager: ITaskManager, tasks: SystemTask
95
95
  taskManagerRef = taskManager;
96
96
 
97
97
  // Run catch-up
98
- const catchUpTasks: CatchUpTask[] = tasks.map((t) => ({
99
- id: t.id,
100
- name: t.name,
101
- schedule: toCoreSchedule(t.schedule),
102
- missedRunPolicy: t.missedRunPolicy,
98
+ const catchUpTasks: CatchUpTask[] = tasks.map((taskDef) => ({
99
+ id: taskDef.id,
100
+ name: taskDef.name,
101
+ schedule: toCoreSchedule(taskDef.schedule),
102
+ missedRunPolicy: taskDef.missedRunPolicy,
103
103
  enabled: true,
104
104
  }));
105
105
  const plan = computeCatchUpPlan(catchUpTasks, stateMap, Date.now());
@@ -117,7 +117,7 @@ export async function initScheduler(taskManager: ITaskManager, tasks: SystemTask
117
117
  runs: plan.runs.length,
118
118
  });
119
119
  for (const run of plan.runs) {
120
- const task = tasks.find((t) => t.id === run.taskId);
120
+ const task = tasks.find((taskDef) => taskDef.id === run.taskId);
121
121
  if (!task) continue;
122
122
  await executeAndLog(task, run.context.scheduledFor, TASK_TRIGGERS.catchUp);
123
123
  }
@@ -137,7 +137,7 @@ export async function initScheduler(taskManager: ITaskManager, tasks: SystemTask
137
137
  }
138
138
 
139
139
  log.info("scheduler", "initialized", {
140
- tasks: tasks.map((t) => t.id),
140
+ tasks: tasks.map((taskDef) => taskDef.id),
141
141
  stateEntries: stateMap.size,
142
142
  });
143
143
  }
@@ -146,7 +146,7 @@ export async function initScheduler(taskManager: ITaskManager, tasks: SystemTask
146
146
  * Updates the in-memory task definition, the task-manager, and
147
147
  * recalculates nextScheduledAt in persisted state. */
148
148
  export async function applyScheduleOverride(taskId: string, schedule: SystemTaskDef["schedule"]): Promise<boolean> {
149
- const task = systemTasks.find((t) => t.id === taskId);
149
+ const task = systemTasks.find((taskDef) => taskDef.id === taskId);
150
150
  if (!task || !taskManagerRef) return false;
151
151
  if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;
152
152
  task.schedule = schedule;
@@ -172,13 +172,13 @@ export function getSchedulerTasks(): Array<{
172
172
  missedRunPolicy: string;
173
173
  state: TaskExecutionState;
174
174
  }> {
175
- return systemTasks.map((t) => ({
176
- id: t.id,
177
- name: t.name,
178
- description: t.description,
179
- schedule: t.schedule,
180
- missedRunPolicy: t.missedRunPolicy,
181
- state: stateMap.get(t.id) ?? emptyState(t.id),
175
+ return systemTasks.map((taskDef) => ({
176
+ id: taskDef.id,
177
+ name: taskDef.name,
178
+ description: taskDef.description,
179
+ schedule: taskDef.schedule,
180
+ missedRunPolicy: taskDef.missedRunPolicy,
181
+ state: stateMap.get(taskDef.id) ?? emptyState(taskDef.id),
182
182
  }));
183
183
  }
184
184
 
@@ -65,8 +65,8 @@ const storelessPending = new Map<string, Set<string>>();
65
65
  let pubsub: IPubSub | null = null;
66
66
  let evictionTimer: ReturnType<typeof setInterval> | null = null;
67
67
 
68
- export function initSessionStore(ps: IPubSub): void {
69
- pubsub = ps;
68
+ export function initSessionStore(pubSubInstance: IPubSub): void {
69
+ pubsub = pubSubInstance;
70
70
  if (evictionTimer) clearInterval(evictionTimer);
71
71
  evictionTimer = setInterval(evictIdleSessions, EVICTION_CHECK_INTERVAL_MS);
72
72
  }
@@ -288,8 +288,8 @@ interface GenerationPayload {
288
288
 
289
289
  const GENERATION_KIND_VALUES: ReadonlySet<string> = new Set(Object.values(GENERATION_KINDS));
290
290
 
291
- function isGenerationKind(v: unknown): v is GenerationKind {
292
- return typeof v === "string" && GENERATION_KIND_VALUES.has(v);
291
+ function isGenerationKind(value: unknown): value is GenerationKind {
292
+ return typeof value === "string" && GENERATION_KIND_VALUES.has(value);
293
293
  }
294
294
 
295
295
  /**
@@ -314,7 +314,7 @@ function applyEventToSession(session: ServerSession, type: string, event: Record
314
314
  timestamp: Date.now(),
315
315
  });
316
316
  } else if (type === EVENT_TYPES.toolCallResult) {
317
- const entry = session.toolCallHistory.find((e) => e.toolUseId === event.toolUseId);
317
+ const entry = session.toolCallHistory.find((historyEntry) => historyEntry.toolUseId === event.toolUseId);
318
318
  if (entry) entry.result = event.content as string;
319
319
  } else if (type === EVENT_TYPES.status) {
320
320
  session.statusMessage = event.message as string;
@@ -404,8 +404,8 @@ export function getSessionImageData(chatSessionId: string): string | undefined {
404
404
 
405
405
  export function getActiveSessionIds(): Set<string> {
406
406
  const ids = new Set<string>();
407
- for (const [id, session] of store) {
408
- if (session.isRunning) ids.add(id);
407
+ for (const [chatSessionId, session] of store) {
408
+ if (session.isRunning) ids.add(chatSessionId);
409
409
  }
410
410
  return ids;
411
411
  }
@@ -465,14 +465,14 @@ function notifySessionsChanged(): void {
465
465
 
466
466
  function evictIdleSessions(): void {
467
467
  const now = Date.now();
468
- for (const [id, session] of store) {
468
+ for (const [chatSessionId, session] of store) {
469
469
  if (session.isRunning) continue;
470
470
  const age = now - new Date(session.updatedAt).getTime();
471
471
  if (age > IDLE_EVICTION_MS) {
472
472
  log.info("session-store", "evicting idle session", {
473
- chatSessionId: id,
473
+ chatSessionId,
474
474
  });
475
- removeSession(id);
475
+ removeSession(chatSessionId);
476
476
  }
477
477
  }
478
478
  }
@@ -52,8 +52,8 @@ function isDue(now: Date, schedule: TaskSchedule, tickMs: number): boolean {
52
52
  }
53
53
 
54
54
  if (schedule.type === SCHEDULE_TYPES.daily) {
55
- const [hh, mm] = schedule.time.split(":").map(Number);
56
- const targetMs = hh * ONE_HOUR_MS + mm * ONE_MINUTE_MS;
55
+ const [hours, minutes] = schedule.time.split(":").map(Number);
56
+ const targetMs = hours * ONE_HOUR_MS + minutes * ONE_MINUTE_MS;
57
57
  const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS;
58
58
  const rounded = Math.floor(msSinceMidnight / tickMs) * tickMs;
59
59
  return rounded === targetMs;
@@ -170,11 +170,11 @@ export function createTaskManager(options?: TaskManagerOptions): ITaskManager {
170
170
  },
171
171
 
172
172
  listTasks() {
173
- return [...registry.values()].map((d) => ({
174
- id: d.id,
175
- description: d.description,
176
- schedule: d.schedule,
177
- dependsOn: d.dependsOn,
173
+ return [...registry.values()].map((taskDef) => ({
174
+ id: taskDef.id,
175
+ description: taskDef.description,
176
+ schedule: taskDef.schedule,
177
+ dependsOn: taskDef.dependsOn,
178
178
  }));
179
179
  },
180
180
  };
package/server/index.ts CHANGED
@@ -157,13 +157,13 @@ app.use(configRoutes);
157
157
  app.use(skillsRoutes);
158
158
  async function listSessionsForBridge(opts: { limit: number; offset: number }) {
159
159
  const rows = await loadAllSessions();
160
- const sorted = rows.sort((a, b) => b.changeMs - a.changeMs);
160
+ const sorted = rows.sort((leftSession, rightSession) => rightSession.changeMs - leftSession.changeMs);
161
161
  const total = sorted.length;
162
- const sessions = sorted.slice(opts.offset, opts.offset + opts.limit).map((r) => ({
163
- id: r.summary.id,
164
- roleId: r.summary.roleId,
165
- preview: r.summary.preview,
166
- updatedAt: r.summary.updatedAt,
162
+ const sessions = sorted.slice(opts.offset, opts.offset + opts.limit).map((row) => ({
163
+ id: row.summary.id,
164
+ roleId: row.summary.roleId,
165
+ preview: row.summary.preview,
166
+ updatedAt: row.summary.updatedAt,
167
167
  }));
168
168
  return { sessions, total };
169
169
  }
@@ -275,8 +275,8 @@ async function ensureCredentialsAvailable(): Promise<void> {
275
275
 
276
276
  if (process.platform === "darwin") {
277
277
  const { refreshCredentials } = await import("./system/credentials.js");
278
- const ok = await refreshCredentials();
279
- if (ok) return;
278
+ const refreshSucceeded = await refreshCredentials();
279
+ if (refreshSucceeded) return;
280
280
  log.error("sandbox", "Failed to export credentials from macOS Keychain. Run `npm run sandbox:login` manually.");
281
281
  process.exit(1);
282
282
  }
@@ -310,14 +310,14 @@ async function setupSandbox(): Promise<boolean> {
310
310
 
311
311
  function logMcpStatus(): void {
312
312
  const enabledMcpTools = mcpTools.filter(isMcpToolEnabled);
313
- const disabledMcpTools = mcpTools.filter((t) => !isMcpToolEnabled(t));
313
+ const disabledMcpTools = mcpTools.filter((toolDef) => !isMcpToolEnabled(toolDef));
314
314
  if (enabledMcpTools.length > 0) {
315
315
  log.info("mcp", "Available", {
316
- tools: enabledMcpTools.map((t) => t.definition.name).join(", "),
316
+ tools: enabledMcpTools.map((toolDef) => toolDef.definition.name).join(", "),
317
317
  });
318
318
  }
319
319
  if (disabledMcpTools.length > 0) {
320
- const names = disabledMcpTools.map((t) => t.definition.name + " (" + (t.requiredEnv ?? []).join(", ") + ")").join(", ");
320
+ const names = disabledMcpTools.map((toolDef) => toolDef.definition.name + " (" + (toolDef.requiredEnv ?? []).join(", ") + ")").join(", ");
321
321
  log.info("mcp", "Unavailable (missing env)", { tools: names });
322
322
  }
323
323
  }
@@ -424,24 +424,24 @@ function startRuntimeServices(httpServer: ReturnType<typeof app.listen>): void {
424
424
  // are silently ignored — the hardcoded defaults above remain.
425
425
  const overrides = loadSchedulerOverrides();
426
426
  for (const task of systemTasks) {
427
- const ovr = overrides[task.id];
428
- if (!ovr) continue;
429
- if (task.schedule.type === SCHEDULE_TYPES.interval && typeof ovr.intervalMs === "number" && ovr.intervalMs > 0) {
427
+ const override = overrides[task.id];
428
+ if (!override) continue;
429
+ if (task.schedule.type === SCHEDULE_TYPES.interval && typeof override.intervalMs === "number" && override.intervalMs > 0) {
430
430
  log.info("scheduler", "applying override", {
431
431
  id: task.id,
432
- intervalMs: ovr.intervalMs,
432
+ intervalMs: override.intervalMs,
433
433
  });
434
434
  task.schedule = {
435
435
  type: SCHEDULE_TYPES.interval,
436
- intervalMs: ovr.intervalMs,
436
+ intervalMs: override.intervalMs,
437
437
  };
438
438
  }
439
- if (task.schedule.type === SCHEDULE_TYPES.daily && typeof ovr.time === "string" && UTC_HH_MM_RE.test(ovr.time)) {
439
+ if (task.schedule.type === SCHEDULE_TYPES.daily && typeof override.time === "string" && UTC_HH_MM_RE.test(override.time)) {
440
440
  log.info("scheduler", "applying override", {
441
441
  id: task.id,
442
- time: ovr.time,
442
+ time: override.time,
443
443
  });
444
- task.schedule = { type: SCHEDULE_TYPES.daily, time: ovr.time };
444
+ task.schedule = { type: SCHEDULE_TYPES.daily, time: override.time };
445
445
  }
446
446
  }
447
447