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
@@ -8,11 +8,11 @@
8
8
  * is a wall-clock question, not a UTC question.
9
9
  */
10
10
  export function toLocalIsoDate(input: Date | number): string {
11
- const d = typeof input === "number" ? new Date(input) : input;
12
- const y = d.getFullYear();
13
- const m = String(d.getMonth() + 1).padStart(2, "0");
14
- const day = String(d.getDate()).padStart(2, "0");
15
- return `${y}-${m}-${day}`;
11
+ const dateValue = typeof input === "number" ? new Date(input) : input;
12
+ const year = dateValue.getFullYear();
13
+ const month = String(dateValue.getMonth() + 1).padStart(2, "0");
14
+ const day = String(dateValue.getDate()).padStart(2, "0");
15
+ return `${year}-${month}-${day}`;
16
16
  }
17
17
 
18
18
  /**
@@ -20,11 +20,11 @@ export function toLocalIsoDate(input: Date | number): string {
20
20
  * any context where the date must not shift with the server's
21
21
  * local timezone.
22
22
  */
23
- export function toUtcIsoDate(ts: Date): string {
24
- const y = ts.getUTCFullYear();
25
- const m = String(ts.getUTCMonth() + 1).padStart(2, "0");
26
- const d = String(ts.getUTCDate()).padStart(2, "0");
27
- return `${y}-${m}-${d}`;
23
+ export function toUtcIsoDate(timestamp: Date): string {
24
+ const year = timestamp.getUTCFullYear();
25
+ const month = String(timestamp.getUTCMonth() + 1).padStart(2, "0");
26
+ const day = String(timestamp.getUTCDate()).padStart(2, "0");
27
+ return `${year}-${month}-${day}`;
28
28
  }
29
29
 
30
30
  /**
@@ -41,16 +41,16 @@ export function isoDateOnly(iso: string): string {
41
41
  * Does NOT validate month/day ranges (Feb 30 passes); that's the
42
42
  * caller's or LLM's responsibility.
43
43
  */
44
- export function isValidIsoDate(s: string): boolean {
45
- if (s.length !== 10) return false;
46
- if (s[4] !== "-" || s[7] !== "-") return false;
47
- return isNumeric(s.slice(0, 4)) && isNumeric(s.slice(5, 7)) && isNumeric(s.slice(8, 10));
44
+ export function isValidIsoDate(input: string): boolean {
45
+ if (input.length !== 10) return false;
46
+ if (input[4] !== "-" || input[7] !== "-") return false;
47
+ return isNumeric(input.slice(0, 4)) && isNumeric(input.slice(5, 7)) && isNumeric(input.slice(8, 10));
48
48
  }
49
49
 
50
- function isNumeric(s: string): boolean {
51
- if (s.length === 0) return false;
52
- for (let i = 0; i < s.length; i++) {
53
- const code = s.charCodeAt(i);
50
+ function isNumeric(input: string): boolean {
51
+ if (input.length === 0) return false;
52
+ for (let index = 0; index < input.length; index++) {
53
+ const code = input.charCodeAt(index);
54
54
  if (code < 48 || code > 57) return false;
55
55
  }
56
56
  return true;
@@ -47,18 +47,18 @@ function isTransientRenameError(err: unknown): boolean {
47
47
  return err.code === "EPERM" || err.code === "EBUSY" || err.code === "EACCES";
48
48
  }
49
49
 
50
- async function renameWithWindowsRetry(from: string, to: string): Promise<void> {
50
+ async function renameWithWindowsRetry(fromPath: string, toPath: string): Promise<void> {
51
51
  for (const delayMs of RENAME_RETRY_DELAYS_MS) {
52
52
  try {
53
- await fs.promises.rename(from, to);
53
+ await fs.promises.rename(fromPath, toPath);
54
54
  return;
55
55
  } catch (err) {
56
56
  if (!isTransientRenameError(err)) throw err;
57
- await new Promise((r) => setTimeout(r, delayMs));
57
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
58
58
  }
59
59
  }
60
60
  // Final attempt — let any error propagate.
61
- await fs.promises.rename(from, to);
61
+ await fs.promises.rename(fromPath, toPath);
62
62
  }
63
63
 
64
64
  // Sync sleep that parks the thread instead of burning CPU. Only
@@ -66,21 +66,21 @@ async function renameWithWindowsRetry(from: string, to: string): Promise<void> {
66
66
  // case block is the sum of RENAME_RETRY_DELAYS_MS (~430ms) and only
67
67
  // triggers under AV/indexer contention.
68
68
  const SYNC_SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
69
- function sleepSync(ms: number): void {
70
- Atomics.wait(SYNC_SLEEP_BUF, 0, 0, ms);
69
+ function sleepSync(millis: number): void {
70
+ Atomics.wait(SYNC_SLEEP_BUF, 0, 0, millis);
71
71
  }
72
72
 
73
- function renameSyncWithWindowsRetry(from: string, to: string): void {
73
+ function renameSyncWithWindowsRetry(fromPath: string, toPath: string): void {
74
74
  for (const delayMs of RENAME_RETRY_DELAYS_MS) {
75
75
  try {
76
- fs.renameSync(from, to);
76
+ fs.renameSync(fromPath, toPath);
77
77
  return;
78
78
  } catch (err) {
79
79
  if (!isTransientRenameError(err)) throw err;
80
80
  sleepSync(delayMs);
81
81
  }
82
82
  }
83
- fs.renameSync(from, to);
83
+ fs.renameSync(fromPath, toPath);
84
84
  }
85
85
 
86
86
  /**
@@ -9,12 +9,12 @@ import { workspacePath } from "../../workspace/paths.js";
9
9
  import { readTextUnder, writeTextUnder } from "./workspace-io.js";
10
10
 
11
11
  const HTML_REL = path.posix.join(WORKSPACE_DIRS.html, "current.html");
12
- const root = (r?: string) => r ?? workspacePath;
12
+ const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
13
13
 
14
- export async function readCurrentHtml(r?: string): Promise<string | null> {
15
- return readTextUnder(root(r), HTML_REL);
14
+ export async function readCurrentHtml(workspaceRoot?: string): Promise<string | null> {
15
+ return readTextUnder(root(workspaceRoot), HTML_REL);
16
16
  }
17
17
 
18
- export async function writeCurrentHtml(html: string, r?: string): Promise<void> {
19
- await writeTextUnder(root(r), HTML_REL, html);
18
+ export async function writeCurrentHtml(html: string, workspaceRoot?: string): Promise<void> {
19
+ await writeTextUnder(root(workspaceRoot), HTML_REL, html);
20
20
  }
@@ -35,8 +35,8 @@ async function safeResolve(relativePath: string): Promise<string> {
35
35
  /** Save raw base64 (no data URI prefix) as a PNG file. Returns the workspace-relative path. */
36
36
  export async function saveImage(base64Data: string): Promise<string> {
37
37
  await ensureImagesDir();
38
- const id = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
39
- const filename = `${id}.png`;
38
+ const imageId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
39
+ const filename = `${imageId}.png`;
40
40
  const absPath = path.join(IMAGES_DIR, filename);
41
41
  await fs.writeFile(absPath, Buffer.from(base64Data, "base64"));
42
42
  return path.posix.join(WORKSPACE_DIRS.images, filename);
@@ -51,9 +51,9 @@ export async function writeJournalState(state: unknown, rootOverride?: string):
51
51
 
52
52
  // ── Index ───────────────────────────────────────────────────────
53
53
 
54
- export async function writeJournalIndex(md: string, rootOverride?: string): Promise<void> {
54
+ export async function writeJournalIndex(markdown: string, rootOverride?: string): Promise<void> {
55
55
  const filePath = path.join(summariesRoot(root(rootOverride)), INDEX_FILE);
56
- await writeFileAtomic(filePath, md);
56
+ await writeFileAtomic(filePath, markdown);
57
57
  }
58
58
 
59
59
  // ── Daily summaries ─────────────────────────────────────────────
@@ -44,7 +44,7 @@ export function buildArtifactPathRandom(dir: string, prefix: string, ext: string
44
44
  // Pass fallbackSlug as slugify's default so it overrides slugify's
45
45
  // built-in "page" default when `prefix` sanitizes to empty.
46
46
  const slug = slugify(prefix, fallbackSlug);
47
- const id = crypto.randomUUID().replace(/-/g, "").slice(0, RANDOM_SUFFIX_LEN);
48
- const fname = `${slug}-${id}${ext}`;
47
+ const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, RANDOM_SUFFIX_LEN);
48
+ const fname = `${slug}-${suffix}${ext}`;
49
49
  return path.posix.join(dir, fname);
50
50
  }
@@ -10,16 +10,16 @@ import { workspacePath } from "../../workspace/paths.js";
10
10
  import { writeFileAtomicSync } from "./atomic.js";
11
11
  import { isEnoent } from "./safe.js";
12
12
 
13
- const root = (r?: string) => r ?? workspacePath;
13
+ const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
14
14
 
15
- function roleFilePath(id: string, r?: string): string {
16
- return path.join(root(r), WORKSPACE_DIRS.roles, `${id}.json`);
15
+ function roleFilePath(roleId: string, workspaceRoot?: string): string {
16
+ return path.join(root(workspaceRoot), WORKSPACE_DIRS.roles, `${roleId}.json`);
17
17
  }
18
18
 
19
19
  /** Check if a custom role file exists. */
20
- export function roleExists(id: string, r?: string): boolean {
20
+ export function roleExists(roleId: string, workspaceRoot?: string): boolean {
21
21
  try {
22
- fs.statSync(roleFilePath(id, r));
22
+ fs.statSync(roleFilePath(roleId, workspaceRoot));
23
23
  return true;
24
24
  } catch {
25
25
  return false;
@@ -27,9 +27,9 @@ export function roleExists(id: string, r?: string): boolean {
27
27
  }
28
28
 
29
29
  /** Delete a custom role file. Returns false if not found. */
30
- export function deleteRole(id: string, r?: string): boolean {
30
+ export function deleteRole(roleId: string, workspaceRoot?: string): boolean {
31
31
  try {
32
- fs.unlinkSync(roleFilePath(id, r));
32
+ fs.unlinkSync(roleFilePath(roleId, workspaceRoot));
33
33
  return true;
34
34
  } catch (err) {
35
35
  if (isEnoent(err)) return false;
@@ -38,8 +38,8 @@ export function deleteRole(id: string, r?: string): boolean {
38
38
  }
39
39
 
40
40
  /** Save (create or overwrite) a custom role file atomically. */
41
- export function saveRole(id: string, data: unknown, r?: string): void {
42
- const dir = path.join(root(r), WORKSPACE_DIRS.roles);
41
+ export function saveRole(roleId: string, data: unknown, workspaceRoot?: string): void {
42
+ const dir = path.join(root(workspaceRoot), WORKSPACE_DIRS.roles);
43
43
  fs.mkdirSync(dir, { recursive: true });
44
- writeFileAtomicSync(roleFilePath(id, r), JSON.stringify(data, null, 2));
44
+ writeFileAtomicSync(roleFilePath(roleId, workspaceRoot), JSON.stringify(data, null, 2));
45
45
  }
@@ -9,12 +9,12 @@ import { resolvePath } from "./workspace-io.js";
9
9
  import { loadJsonFile } from "./json.js";
10
10
  import { writeFileAtomicSync } from "./atomic.js";
11
11
 
12
- const root = (r?: string) => r ?? workspacePath;
12
+ const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
13
13
 
14
- export function loadSchedulerItems<T>(fallback: T, r?: string): T {
15
- return loadJsonFile(resolvePath(root(r), WORKSPACE_FILES.schedulerItems), fallback);
14
+ export function loadSchedulerItems<T>(fallback: T, workspaceRoot?: string): T {
15
+ return loadJsonFile(resolvePath(root(workspaceRoot), WORKSPACE_FILES.schedulerItems), fallback);
16
16
  }
17
17
 
18
- export function saveSchedulerItems(items: unknown, r?: string): void {
19
- writeFileAtomicSync(resolvePath(root(r), WORKSPACE_FILES.schedulerItems), JSON.stringify(items, null, 2));
18
+ export function saveSchedulerItems(items: unknown, workspaceRoot?: string): void {
19
+ writeFileAtomicSync(resolvePath(root(workspaceRoot), WORKSPACE_FILES.schedulerItems), JSON.stringify(items, null, 2));
20
20
  }
@@ -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
  }
@@ -36,8 +36,8 @@ 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`;
39
+ const sheetId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
40
+ const filename = `${sheetId}.json`;
41
41
  await fs.writeFile(path.join(SPREADSHEETS_DIR, filename), JSON.stringify(sheets), "utf-8");
42
42
  return path.posix.join(WORKSPACE_DIRS.spreadsheets, filename);
43
43
  }
@@ -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
  }
@@ -52,8 +52,8 @@ export async function readManifest(workspaceRoot: string): Promise<ChatIndexMani
52
52
 
53
53
  function isManifest(raw: unknown): raw is ChatIndexManifest {
54
54
  if (!isRecord(raw)) return false;
55
- const o = raw as Record<string, unknown>;
56
- return o.version === 1 && Array.isArray(o.entries);
55
+ const manifestRecord = raw as Record<string, unknown>;
56
+ return manifestRecord.version === 1 && Array.isArray(manifestRecord.entries);
57
57
  }
58
58
 
59
59
  // In-process mutex serializing the read-modify-write sequence on
@@ -64,7 +64,7 @@ function isManifest(raw: unknown): raw is ChatIndexManifest {
64
64
  // this module's single-process assumption.
65
65
  let manifestMutex: Promise<void> = Promise.resolve();
66
66
 
67
- async function withManifestLock<T>(fn: () => Promise<T>): Promise<T> {
67
+ async function withManifestLock<T>(lockedFn: () => Promise<T>): Promise<T> {
68
68
  const prev = manifestMutex;
69
69
  let release: () => void = () => {};
70
70
  manifestMutex = new Promise<void>((resolve) => {
@@ -72,7 +72,7 @@ async function withManifestLock<T>(fn: () => Promise<T>): Promise<T> {
72
72
  });
73
73
  try {
74
74
  await prev;
75
- return await fn();
75
+ return await lockedFn();
76
76
  } finally {
77
77
  release();
78
78
  }
@@ -83,12 +83,12 @@ async function withManifestLock<T>(fn: () => Promise<T>): Promise<T> {
83
83
  // already serializes callers within this process, but a unique
84
84
  // name means the rename can't collide even if a stray .tmp file
85
85
  // is left behind by a previous crashed run.
86
- async function writeManifestAtomic(workspaceRoot: string, m: ChatIndexManifest): Promise<void> {
86
+ async function writeManifestAtomic(workspaceRoot: string, manifest: ChatIndexManifest): Promise<void> {
87
87
  // `uniqueTmp` belt-and-suspenders: the in-process mutex above
88
88
  // already serializes callers, but a unique tmp name means the
89
89
  // rename can't collide even if a stray .tmp file is left behind
90
90
  // by a previous crashed run.
91
- await writeJsonAtomic(manifestPathFor(workspaceRoot), m, {
91
+ await writeJsonAtomic(manifestPathFor(workspaceRoot), manifest, {
92
92
  uniqueTmp: true,
93
93
  });
94
94
  }
@@ -117,9 +117,9 @@ export async function isFresh(workspaceRoot: string, sessionId: string, now: num
117
117
  if (!isRecord(entry)) return false;
118
118
  const indexedAt = (entry as Record<string, unknown>).indexedAt;
119
119
  if (typeof indexedAt !== "string") return false;
120
- const ts = Date.parse(indexedAt);
121
- if (Number.isNaN(ts)) return false;
122
- return now - ts < minIntervalMs;
120
+ const indexedTimestamp = Date.parse(indexedAt);
121
+ if (Number.isNaN(indexedTimestamp)) return false;
122
+ return now - indexedTimestamp < minIntervalMs;
123
123
  } catch {
124
124
  return false;
125
125
  }
@@ -137,10 +137,10 @@ async function readSessionMeta(workspaceRoot: string, sessionId: string): Promis
137
137
  const raw = await readFile(sessionMetaPathFor(workspaceRoot, sessionId), "utf-8");
138
138
  const parsed: unknown = JSON.parse(raw);
139
139
  if (!isRecord(parsed)) return {};
140
- const o = parsed as Record<string, unknown>;
140
+ const metaRecord = parsed as Record<string, unknown>;
141
141
  return {
142
- roleId: typeof o.roleId === "string" ? o.roleId : undefined,
143
- startedAt: typeof o.startedAt === "string" ? o.startedAt : undefined,
142
+ roleId: typeof metaRecord.roleId === "string" ? metaRecord.roleId : undefined,
143
+ startedAt: typeof metaRecord.startedAt === "string" ? metaRecord.startedAt : undefined,
144
144
  };
145
145
  } catch {
146
146
  return {};
@@ -152,7 +152,7 @@ async function readSessionMeta(workspaceRoot: string, sessionId: string): Promis
152
152
  export async function listSessionIds(workspaceRoot: string): Promise<string[]> {
153
153
  try {
154
154
  const files = await readdir(chatDirFor(workspaceRoot));
155
- return files.filter((f) => f.endsWith(".jsonl")).map((f) => f.slice(0, -".jsonl".length));
155
+ return files.filter((fileName) => fileName.endsWith(".jsonl")).map((fileName) => fileName.slice(0, -".jsonl".length));
156
156
  } catch {
157
157
  return [];
158
158
  }
@@ -199,9 +199,9 @@ export async function indexSession(workspaceRoot: string, sessionId: string, dep
199
199
  // Upsert into manifest under the in-process lock: replace any
200
200
  // prior entry with the same id, sort newest-first by startedAt.
201
201
  await updateManifest(workspaceRoot, (current) => {
202
- const filtered = current.entries.filter((e) => e.id !== sessionId);
202
+ const filtered = current.entries.filter((entryItem) => entryItem.id !== sessionId);
203
203
  filtered.push(entry);
204
- filtered.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt));
204
+ filtered.sort((leftEntry, rightEntry) => Date.parse(rightEntry.startedAt) - Date.parse(leftEntry.startedAt));
205
205
  return { version: 1, entries: filtered };
206
206
  });
207
207
 
@@ -45,8 +45,8 @@ const CONTROL_CHAR_RE_G = /[\x00-\x1f]/g;
45
45
 
46
46
  // ── Validation ──────────────────────────────────────────────────
47
47
 
48
- function isValidStructure(v: unknown): v is DirStructure {
49
- return v === DIR_STRUCTURES.flat || v === DIR_STRUCTURES.byName || v === DIR_STRUCTURES.byDate;
48
+ function isValidStructure(value: unknown): value is DirStructure {
49
+ return value === DIR_STRUCTURES.flat || value === DIR_STRUCTURES.byName || value === DIR_STRUCTURES.byDate;
50
50
  }
51
51
 
52
52
  function validatePath(rawPath: string): string | null {
@@ -55,7 +55,7 @@ function validatePath(rawPath: string): string | null {
55
55
  const normalized = path.posix.normalize(rawPath);
56
56
 
57
57
  // Must start with allowed prefix
58
- if (!ALLOWED_PREFIXES.some((p) => normalized.startsWith(p))) return null;
58
+ if (!ALLOWED_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return null;
59
59
 
60
60
  // No path traversal
61
61
  if (normalized.includes("..")) return null;
@@ -64,7 +64,7 @@ function validatePath(rawPath: string): string | null {
64
64
  if (path.isAbsolute(normalized)) return null;
65
65
 
66
66
  // Not a reserved system directory
67
- if (RESERVED_DIRS.some((r) => normalized === r || normalized.startsWith(r + "/"))) {
67
+ if (RESERVED_DIRS.some((reservedDir) => normalized === reservedDir || normalized.startsWith(reservedDir + "/"))) {
68
68
  return null;
69
69
  }
70
70
 
@@ -112,7 +112,7 @@ export function loadCustomDirs(root?: string): CustomDirEntry[] {
112
112
  const entries = parsed
113
113
  .slice(0, MAX_ENTRIES)
114
114
  .map(validateEntry)
115
- .filter((e): e is CustomDirEntry => e !== null);
115
+ .filter((entry): entry is CustomDirEntry => entry !== null);
116
116
 
117
117
  const skipped = parsed.length - entries.length;
118
118
  if (skipped > 0) {
@@ -153,8 +153,8 @@ export function validateCustomDirs(raw: unknown): { entries: CustomDirEntry[] }
153
153
  if (entry) {
154
154
  entries.push(entry);
155
155
  } else {
156
- const p = isRecord(item) ? String((item as Record<string, unknown>).path ?? "") : "";
157
- errors.push(`entry ${i}: invalid path "${p}"`);
156
+ const itemPath = isRecord(item) ? String((item as Record<string, unknown>).path ?? "") : "";
157
+ errors.push(`entry ${i}: invalid path "${itemPath}"`);
158
158
  }
159
159
  });
160
160
  if (errors.length > 0) {
@@ -205,14 +205,14 @@ export function buildCustomDirsPrompt(entries: readonly CustomDirEntry[]): strin
205
205
  "",
206
206
  ];
207
207
 
208
- for (const e of entries) {
208
+ for (const entry of entries) {
209
209
  const structureHint =
210
- e.structure === DIR_STRUCTURES.byName
210
+ entry.structure === DIR_STRUCTURES.byName
211
211
  ? " (organize by name in subfolders)"
212
- : e.structure === DIR_STRUCTURES.byDate
212
+ : entry.structure === DIR_STRUCTURES.byDate
213
213
  ? " (organize by date: YYYY/MM/DD/)"
214
214
  : "";
215
- lines.push(`- \`${e.path}/\`${structureHint} — ${e.description}`);
215
+ lines.push(`- \`${entry.path}/\`${structureHint} — ${entry.description}`);
216
216
  }
217
217
 
218
218
  lines.push("");