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
package/client/index.html CHANGED
@@ -17,10 +17,10 @@
17
17
  <title>MulmoClaude</title>
18
18
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='30' height='30' x='1' y='1' rx='6' fill='%236B7280'/><text x='16' y='17' text-anchor='middle' dominant-baseline='central' font-family='sans-serif' font-weight='bold' font-size='20' fill='white'>M</text></svg>" />
19
19
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
20
- <script type="module" crossorigin src="/assets/index-D8rhwXLq.js"></script>
20
+ <script type="module" crossorigin src="/assets/index-eHWB79u5.js"></script>
21
21
  <link rel="modulepreload" crossorigin href="/assets/chunk-vKJrgz-R-C_I3GbVV.js">
22
22
  <link rel="modulepreload" crossorigin href="/assets/typeof-DBp4T-Ny-BC0P-2DM.js">
23
- <link rel="stylesheet" crossorigin href="/assets/index-KNLBjwuh.css">
23
+ <link rel="stylesheet" crossorigin href="/assets/index-Bm70FDU2.css">
24
24
  </head>
25
25
  <body>
26
26
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmoclaude",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "MulmoClaude — GUI-chat with Claude Code + long-term memory. One command to start.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,10 +14,10 @@ export const CONTAINER_WORKSPACE_PATH = "/home/node/mulmoclaude";
14
14
 
15
15
  const BASE_ALLOWED_TOOLS = ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebFetch", "WebSearch"];
16
16
 
17
- const MCP_PLUGINS = new Set([...MCP_PLUGIN_NAMES, ...mcpTools.filter(isMcpToolEnabled).map((t) => t.definition.name)]);
17
+ const MCP_PLUGINS = new Set([...MCP_PLUGIN_NAMES, ...mcpTools.filter(isMcpToolEnabled).map((toolDef) => toolDef.definition.name)]);
18
18
 
19
19
  export function getActivePlugins(role: Role): string[] {
20
- return role.availablePlugins.filter((p) => MCP_PLUGINS.has(p));
20
+ return role.availablePlugins.filter((pluginName) => MCP_PLUGINS.has(pluginName));
21
21
  }
22
22
 
23
23
  export interface McpConfigParams {
@@ -71,12 +71,12 @@ function prepareUserStdioServer(spec: Extract<McpServerSpec, { type: "stdio" }>,
71
71
 
72
72
  export function prepareUserServers(userServers: Record<string, McpServerSpec>, useDocker: boolean, hostWorkspacePath: string): Record<string, McpServerSpec> {
73
73
  const out: Record<string, McpServerSpec> = {};
74
- for (const [id, spec] of Object.entries(userServers)) {
74
+ for (const [serverId, spec] of Object.entries(userServers)) {
75
75
  if (spec.enabled === false) continue;
76
76
  if (spec.type === "http") {
77
- out[id] = prepareUserHttpServer(spec, useDocker);
77
+ out[serverId] = prepareUserHttpServer(spec, useDocker);
78
78
  } else {
79
- out[id] = prepareUserStdioServer(spec, useDocker, hostWorkspacePath);
79
+ out[serverId] = prepareUserStdioServer(spec, useDocker, hostWorkspacePath);
80
80
  }
81
81
  }
82
82
  return out;
@@ -137,9 +137,9 @@ function buildMulmoclaudeServer(params: { chatSessionId: string; port: number; a
137
137
  // defence-in-depth.
138
138
  function excludeReservedKeys(servers: Record<string, McpServerSpec>): Record<string, McpServerSpec> {
139
139
  const out: Record<string, McpServerSpec> = {};
140
- for (const [id, spec] of Object.entries(servers)) {
141
- if (id === "mulmoclaude") continue;
142
- out[id] = spec;
140
+ for (const [serverId, spec] of Object.entries(servers)) {
141
+ if (serverId === "mulmoclaude") continue;
142
+ out[serverId] = spec;
143
143
  }
144
144
  return out;
145
145
  }
@@ -165,12 +165,12 @@ export function buildMcpConfig(params: McpConfigParams): object {
165
165
  // we're running natively (since the sandbox image is minimal in Docker).
166
166
  export function userServerAllowedToolNames(userServers: Record<string, McpServerSpec>, useDocker: boolean): string[] {
167
167
  const names: string[] = [];
168
- for (const [id, spec] of Object.entries(userServers)) {
168
+ for (const [serverId, spec] of Object.entries(userServers)) {
169
169
  if (spec.enabled === false) continue;
170
170
  // Stdio servers are dropped under Docker because the sandbox
171
171
  // image is too minimal to run most of them (see #162).
172
172
  if (spec.type === "stdio" && useDocker) continue;
173
- names.push(`mcp__${id}`);
173
+ names.push(`mcp__${serverId}`);
174
174
  }
175
175
  return names;
176
176
  }
@@ -188,7 +188,7 @@ export interface CliArgsParams {
188
188
  export function buildCliArgs(params: CliArgsParams): string[] {
189
189
  const { systemPrompt, activePlugins, claudeSessionId, mcpConfigPath, extraAllowedTools = [] } = params;
190
190
 
191
- const mcpToolNames = activePlugins.map((p) => `mcp__mulmoclaude__${p}`);
191
+ const mcpToolNames = activePlugins.map((pluginName) => `mcp__mulmoclaude__${pluginName}`);
192
192
  const allowedTools = [...BASE_ALLOWED_TOOLS, ...extraAllowedTools, ...mcpToolNames];
193
193
 
194
194
  // stream-json input mode: the user message is streamed through
@@ -351,7 +351,7 @@ export function buildDockerSpawnArgs(params: DockerSpawnArgsParams): string[] {
351
351
  sandboxAuthArgs = [],
352
352
  sshAgentForward = false,
353
353
  } = params;
354
- const toDockerPath = (p: string): string => p.replace(/\\/g, "/");
354
+ const toDockerPath = (hostPath: string): string => hostPath.replace(/\\/g, "/");
355
355
  const extraHosts: string[] = platform === "linux" ? ["--add-host", "host.docker.internal:host-gateway"] : [];
356
356
 
357
357
  return [
@@ -30,7 +30,7 @@ interface JsonRpcMessage {
30
30
  params?: ToolCallParams;
31
31
  }
32
32
 
33
- const isJsonRpcMessage = (v: unknown): v is JsonRpcMessage => isRecord(v) && "method" in v;
33
+ const isJsonRpcMessage = (value: unknown): value is JsonRpcMessage => isRecord(value) && "method" in value;
34
34
 
35
35
  const SESSION_ID = env.mcpSessionId;
36
36
  const PORT = env.port;
@@ -80,12 +80,12 @@ function fromPackage(def: ToolDefinition, endpoint: string): ToolDef {
80
80
 
81
81
  // Pure MCP tools (no GUI) — auto-registered from server/mcp-tools/
82
82
  const mcpToolDefs: Record<string, ToolDef> = Object.fromEntries(
83
- mcpTools.filter(isMcpToolEnabled).map((t) => [
84
- t.definition.name,
83
+ mcpTools.filter(isMcpToolEnabled).map((toolDef) => [
84
+ toolDef.definition.name,
85
85
  {
86
- name: t.definition.name,
87
- description: t.definition.description,
88
- inputSchema: t.definition.inputSchema,
86
+ name: toolDef.definition.name,
87
+ description: toolDef.definition.description,
88
+ inputSchema: toolDef.definition.inputSchema,
89
89
  },
90
90
  ]),
91
91
  );
@@ -303,7 +303,7 @@ async function handleToolCall(name: string, args: Record<string, unknown>): Prom
303
303
  // Pure MCP tools — call via /api/mcp-tools/:tool, return text directly
304
304
  // (no frontend push). Opt out of postJson's HTTP error throw because
305
305
  // we want to surface the JSON error body to the caller as a string.
306
- const mcpTool = mcpTools.find((t) => t.definition.name === name);
306
+ const mcpTool = mcpTools.find((toolDef) => toolDef.definition.name === name);
307
307
  if (mcpTool) {
308
308
  const res = await postJson(`/api/mcp-tools/${name}`, args, {
309
309
  allowHttpError: true,
@@ -313,7 +313,7 @@ async function handleToolCall(name: string, args: Record<string, unknown>): Prom
313
313
  return typeof json.result === "string" ? json.result : JSON.stringify(json.result);
314
314
  }
315
315
 
316
- const tool = tools.find((t) => t.name === name);
316
+ const tool = tools.find((toolDef) => toolDef.name === name);
317
317
  if (!tool) throw new Error(`Unknown tool: ${name}`);
318
318
 
319
319
  const res = await postJson(tool.endpoint!, args);
@@ -347,12 +347,12 @@ process.stdin.on("data", (chunk: Buffer) => {
347
347
  }
348
348
  if (!isJsonRpcMessage(msg)) continue;
349
349
 
350
- const { id, method, params } = msg;
350
+ const { id: requestId, method, params } = msg;
351
351
 
352
352
  if (method === "initialize") {
353
353
  respond({
354
354
  jsonrpc: "2.0",
355
- id,
355
+ id: requestId,
356
356
  result: {
357
357
  protocolVersion: "2024-11-05",
358
358
  capabilities: { tools: {} },
@@ -362,12 +362,12 @@ process.stdin.on("data", (chunk: Buffer) => {
362
362
  } else if (method === "tools/list") {
363
363
  respond({
364
364
  jsonrpc: "2.0",
365
- id,
365
+ id: requestId,
366
366
  result: {
367
- tools: tools.map((t) => ({
368
- name: t.name,
369
- description: t.description,
370
- inputSchema: t.inputSchema,
367
+ tools: tools.map((toolDef) => ({
368
+ name: toolDef.name,
369
+ description: toolDef.description,
370
+ inputSchema: toolDef.inputSchema,
371
371
  })),
372
372
  },
373
373
  });
@@ -375,7 +375,7 @@ process.stdin.on("data", (chunk: Buffer) => {
375
375
  if (!params?.name) {
376
376
  respond({
377
377
  jsonrpc: "2.0",
378
- id,
378
+ id: requestId,
379
379
  error: {
380
380
  code: -32602,
381
381
  message: "Invalid params: tools/call requires params.name",
@@ -388,14 +388,14 @@ process.stdin.on("data", (chunk: Buffer) => {
388
388
  .then((text) => {
389
389
  respond({
390
390
  jsonrpc: "2.0",
391
- id,
391
+ id: requestId,
392
392
  result: { content: [{ type: "text", text }] },
393
393
  });
394
394
  })
395
395
  .catch((err: unknown) => {
396
396
  respond({
397
397
  jsonrpc: "2.0",
398
- id,
398
+ id: requestId,
399
399
  result: {
400
400
  content: [{ type: "text", text: String(err) }],
401
401
  isError: true,
@@ -403,7 +403,7 @@ process.stdin.on("data", (chunk: Buffer) => {
403
403
  });
404
404
  });
405
405
  } else if (method === "ping") {
406
- respond({ jsonrpc: "2.0", id, result: {} });
406
+ respond({ jsonrpc: "2.0", id: requestId, result: {} });
407
407
  }
408
408
  // notifications/initialized and other notifications: no response needed
409
409
  }
@@ -64,7 +64,7 @@ function formatTweet(tweet: XTweet, author?: XUser, url?: string): string {
64
64
  : "";
65
65
  const link = url ?? "";
66
66
  return [byline, "", tweet.text, "", metrics, link]
67
- .filter((l) => l !== undefined)
67
+ .filter((line) => line !== undefined)
68
68
  .join("\n")
69
69
  .trimEnd();
70
70
  }
@@ -104,12 +104,12 @@ export const readXPost = {
104
104
  return errorMessage(err);
105
105
  }
106
106
 
107
- if (data.errors?.length) return `X API error: ${data.errors.map((e) => e.detail).join("; ")}`;
107
+ if (data.errors?.length) return `X API error: ${data.errors.map((err) => err.detail).join("; ")}`;
108
108
 
109
109
  const tweet = data.data as XTweet | undefined;
110
110
  if (!tweet) return "Tweet not found.";
111
111
 
112
- const author = data.includes?.users?.find((u) => u.id === tweet.author_id);
112
+ const author = data.includes?.users?.find((user) => user.id === tweet.author_id);
113
113
  const canonicalUrl = author ? `https://x.com/${author.username}/status/${tweet.id}` : undefined;
114
114
  return formatTweet(tweet, author, canonicalUrl);
115
115
  },
@@ -168,13 +168,13 @@ export const searchX = {
168
168
  return errorMessage(err);
169
169
  }
170
170
 
171
- if (data.errors?.length) return `X API error: ${data.errors.map((e) => e.detail).join("; ")}`;
171
+ if (data.errors?.length) return `X API error: ${data.errors.map((err) => err.detail).join("; ")}`;
172
172
 
173
173
  const tweets = Array.isArray(data.data) ? data.data : [];
174
174
  if (tweets.length === 0) return `No recent posts found for: "${query}"`;
175
175
 
176
176
  const users = data.includes?.users ?? [];
177
- const userMap = new Map(users.map((u) => [u.id, u]));
177
+ const userMap = new Map(users.map((user) => [user.id, user]));
178
178
 
179
179
  const lines: string[] = [`Search: "${query}" — ${tweets.length} result${tweets.length !== 1 ? "s" : ""}`, ""];
180
180
  tweets.forEach((tweet, i) => {
@@ -253,12 +253,17 @@ export function buildPluginPromptSections(role: Role): string[] {
253
253
  // Some package plugins use an older gui-chat-protocol without the `prompt`
254
254
  // field, so access it via `in` check to keep TypeScript happy.
255
255
  const defPrompts = Object.fromEntries(
256
- PLUGIN_DEFS.filter((d) => "prompt" in d && d.prompt && allowedPlugins.has(d.name)).map((d) => [d.name, (d as unknown as { prompt: string }).prompt]),
256
+ PLUGIN_DEFS.filter((definition) => "prompt" in definition && definition.prompt && allowedPlugins.has(definition.name)).map((definition) => [
257
+ definition.name,
258
+ (definition as unknown as { prompt: string }).prompt,
259
+ ]),
257
260
  );
258
261
 
259
262
  // Collect prompts from MCP tools
260
263
  const mcpToolPrompts = Object.fromEntries(
261
- mcpTools.filter((t) => t.prompt && allowedPlugins.has(t.definition.name) && isMcpToolEnabled(t)).map((t) => [t.definition.name, t.prompt as string]),
264
+ mcpTools
265
+ .filter((toolDef) => toolDef.prompt && allowedPlugins.has(toolDef.definition.name) && isMcpToolEnabled(toolDef))
266
+ .map((toolDef) => [toolDef.definition.name, toolDef.prompt as string]),
262
267
  );
263
268
 
264
269
  // MCP tool prompts override definition prompts if both exist
@@ -310,7 +315,7 @@ function buildInlinedHelpFiles(rolePrompt: string, workspacePath: string): strin
310
315
  // Read() the stale legacy location.
311
316
  return content ? `### ${WORKSPACE_DIRS.helps}/${name}\n\n${content}` : null;
312
317
  })
313
- .filter((s): s is string => s !== null);
318
+ .filter((section): section is string => section !== null);
314
319
  }
315
320
 
316
321
  // Wrap a list of sub-entries under a single markdown heading, or
@@ -345,5 +350,5 @@ export function buildSystemPrompt(params: SystemPromptParams): string {
345
350
  headingSection("Plugin Instructions", buildPluginPromptSections(role)),
346
351
  ];
347
352
 
348
- return sections.filter((s): s is string => s !== null).join("\n\n");
353
+ return sections.filter((section): section is string => section !== null).join("\n\n");
349
354
  }
@@ -48,7 +48,7 @@ export interface SandboxMountSpec {
48
48
  */
49
49
  export function buildAllowedConfigMounts(home: string = homedir()): Record<string, SandboxMountSpec> {
50
50
  return {
51
- gh: {
51
+ ["gh"]: {
52
52
  name: "gh",
53
53
  hostPath: path.join(home, ".config", "gh"),
54
54
  containerPath: "/home/node/.config/gh",
@@ -261,7 +261,7 @@ export function resolveSandboxAuth(params: ResolveSandboxAuthParams): ResolvedSa
261
261
  const args = [...configMountArgs(parsed.resolved), ...sshResult.args, ...sshAllowedHostsArgs, ...ghTokenArgs.args];
262
262
  const allowedHostsSuffix = sshResult.args.length > 0 && params.sshAllowedHosts ? ` → hosts: ${params.sshAllowedHosts}` : "";
263
263
  const appliedDescriptions = [
264
- ...parsed.resolved.map((s) => `${s.name} (${s.description})`),
264
+ ...parsed.resolved.map((spec) => `${spec.name} (${spec.description})`),
265
265
  ...(sshResult.args.length > 0 ? [`ssh-agent forward${allowedHostsSuffix}`] : []),
266
266
  ...(ghTokenArgs.args.length > 0 ? ["gh CLI (GH_TOKEN fallback)"] : []),
267
267
  ];
@@ -284,7 +284,7 @@ export function resolveSandboxAuth(params: ResolveSandboxAuthParams): ResolvedSa
284
284
  // GH_TOKEN env var. This only runs when "gh" was explicitly
285
285
  // requested (#259 opt-in principle).
286
286
  function resolveGhTokenFallback(requestedNames: readonly string[], parsed: ParsedMountList): { args: string[] } {
287
- const ghRequested = requestedNames.some((n) => n.trim() === "gh");
287
+ const ghRequested = requestedNames.some((name) => name.trim() === "gh");
288
288
  if (!ghRequested) return { args: [] };
289
289
 
290
290
  // If an explicit GH_TOKEN is already in the environment, pass it.
@@ -295,8 +295,8 @@ function resolveGhTokenFallback(requestedNames: readonly string[], parsed: Parse
295
295
  // If the file mount resolved (hosts.yml exists), the token might
296
296
  // be in the file. Check if it's keyring-based by looking for
297
297
  // "oauth_token" in the hosts.yml — if missing, fall back.
298
- const ghResolved = parsed.resolved.some((s) => s.name === "gh");
299
- const ghMissing = parsed.missing.some((s) => s.name === "gh");
298
+ const ghResolved = parsed.resolved.some((spec) => spec.name === "gh");
299
+ const ghMissing = parsed.missing.some((spec) => spec.name === "gh");
300
300
 
301
301
  // gh dir doesn't exist at all → try extracting from keyring
302
302
  // gh dir exists (mounted) → still try, since keyring auth leaves
@@ -324,6 +324,6 @@ function resolveGhTokenFallback(requestedNames: readonly string[], parsed: Parse
324
324
  // Docker accepts POSIX-style paths even on Windows when using
325
325
  // Docker Desktop, and the rest of the codebase already uses this
326
326
  // helper in buildDockerSpawnArgs.
327
- function toDockerPath(p: string): string {
328
- return p.replace(/\\/g, "/");
327
+ function toDockerPath(hostPath: string): string {
328
+ return hostPath.replace(/\\/g, "/");
329
329
  }
@@ -98,7 +98,7 @@ function extractTextDelta(event: RawStreamEvent): string | null {
98
98
  // Filter assistant block events: when deltas already streamed the
99
99
  // text, remove text-type events to prevent duplication.
100
100
  function filterAssistantBlocks(blockEvents: AgentEvent[], deltaStreamed: boolean): AgentEvent[] {
101
- return deltaStreamed ? blockEvents.filter((e) => e.type !== EVENT_TYPES.text) : blockEvents;
101
+ return deltaStreamed ? blockEvents.filter((agentEvent) => agentEvent.type !== EVENT_TYPES.text) : blockEvents;
102
102
  }
103
103
 
104
104
  // Stateful parser that deduplicates text across the three stages
@@ -151,11 +151,11 @@ export function createStreamParser(): {
151
151
  }
152
152
 
153
153
  const content = event.message?.content;
154
- const blockEvents = Array.isArray(content) ? content.map(blockToEvent).filter((e): e is AgentEvent => e !== null) : [];
154
+ const blockEvents = Array.isArray(content) ? content.map(blockToEvent).filter((agentEvent): agentEvent is AgentEvent => agentEvent !== null) : [];
155
155
 
156
156
  if (event.type === "assistant") {
157
157
  const filtered = filterAssistantBlocks(blockEvents, textStreamedFromDeltas);
158
- if (filtered.some((e) => e.type === EVENT_TYPES.text)) {
158
+ if (filtered.some((agentEvent) => agentEvent.type === EVENT_TYPES.text)) {
159
159
  textEmitted = true;
160
160
  }
161
161
  return [{ type: EVENT_TYPES.status, message: "Thinking..." }, ...filtered];
@@ -185,7 +185,7 @@ export function parseStreamEvent(event: RawStreamEvent): AgentEvent[] {
185
185
  }
186
186
 
187
187
  const content = event.message?.content;
188
- const blockEvents = Array.isArray(content) ? content.map(blockToEvent).filter((e): e is AgentEvent => e !== null) : [];
188
+ const blockEvents = Array.isArray(content) ? content.map(blockToEvent).filter((agentEvent): agentEvent is AgentEvent => agentEvent !== null) : [];
189
189
 
190
190
  if (event.type === "assistant") {
191
191
  return [{ type: EVENT_TYPES.status, message: "Thinking..." }, ...blockEvents];
@@ -230,7 +230,7 @@ function resolveRefPath(prefixedPath: string): string | null {
230
230
  const remainder = slashIdx >= 0 ? afterPrefix.slice(slashIdx + 1) : "";
231
231
 
232
232
  const entries = getCachedReferenceDirs();
233
- const entry = entries.find((e) => e.label === label);
233
+ const entry = entries.find((refEntry) => refEntry.label === label);
234
234
  if (!entry) return null;
235
235
 
236
236
  let rootReal: string;
@@ -393,10 +393,10 @@ export async function buildTreeAsync(absPath: string, relPath: string, gitFilter
393
393
  return buildTreeAsync(childAbs, childRel, localFilter);
394
394
  });
395
395
  const resolved = await Promise.all(childPromises);
396
- const children = resolved.filter((c): c is TreeNode => c !== null);
397
- children.sort((a, b) => {
398
- if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
399
- return a.name.localeCompare(b.name);
396
+ const children = resolved.filter((childNode): childNode is TreeNode => childNode !== null);
397
+ children.sort((leftChild, rightChild) => {
398
+ if (leftChild.type !== rightChild.type) return leftChild.type === "dir" ? -1 : 1;
399
+ return leftChild.name.localeCompare(rightChild.name);
400
400
  });
401
401
  return {
402
402
  name: relPath ? path.basename(relPath) : "",
@@ -460,10 +460,10 @@ export async function listDirShallow(absPath: string, relPath: string, gitFilter
460
460
  };
461
461
  });
462
462
  const resolved = await Promise.all(childPromises);
463
- const children = resolved.filter((c): c is TreeNode => c !== null);
464
- children.sort((a, b) => {
465
- if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
466
- return a.name.localeCompare(b.name);
463
+ const children = resolved.filter((childNode): childNode is TreeNode => childNode !== null);
464
+ children.sort((leftChild, rightChild) => {
465
+ if (leftChild.type !== rightChild.type) return leftChild.type === "dir" ? -1 : 1;
466
+ return leftChild.name.localeCompare(rightChild.name);
467
467
  });
468
468
  return {
469
469
  name: relPath ? path.basename(relPath) : "",
@@ -93,11 +93,11 @@ async function handleTaskAction(action: string, input: Record<string, unknown>,
93
93
  }
94
94
 
95
95
  if (action === SCHEDULER_ACTIONS.deleteTask) {
96
- const id = typeof input.id === "string" ? input.id : "";
96
+ const taskId = typeof input.id === "string" ? input.id : "";
97
97
  const tasks = loadUserTasks();
98
- const idx = tasks.findIndex((t) => t.id === id);
98
+ const idx = tasks.findIndex((task) => task.id === taskId);
99
99
  if (idx === -1) {
100
- res.status(404).json({ error: `task not found: ${id}` });
100
+ res.status(404).json({ error: `task not found: ${taskId}` });
101
101
  return;
102
102
  }
103
103
  const name = tasks[idx].name;
@@ -107,17 +107,17 @@ async function handleTaskAction(action: string, input: Record<string, unknown>,
107
107
  res.json({
108
108
  uuid: crypto.randomUUID(),
109
109
  message: `Task "${name}" deleted.`,
110
- data: { deleted: id },
110
+ data: { deleted: taskId },
111
111
  });
112
112
  return;
113
113
  }
114
114
 
115
115
  if (action === SCHEDULER_ACTIONS.runTask) {
116
- const id = typeof input.id === "string" ? input.id : "";
116
+ const taskId = typeof input.id === "string" ? input.id : "";
117
117
  const tasks = loadUserTasks();
118
- const task = tasks.find((t) => t.id === id);
118
+ const task = tasks.find((candidate) => candidate.id === taskId);
119
119
  if (!task) {
120
- res.status(404).json({ error: `task not found: ${id}` });
120
+ res.status(404).json({ error: `task not found: ${taskId}` });
121
121
  return;
122
122
  }
123
123
  const chatSessionId = crypto.randomUUID();
@@ -138,7 +138,7 @@ async function handleTaskAction(action: string, input: Record<string, unknown>,
138
138
  res.json({
139
139
  uuid: crypto.randomUUID(),
140
140
  message: `Task "${task.name}" triggered.`,
141
- data: { triggered: id, chatSessionId },
141
+ data: { triggered: taskId, chatSessionId },
142
142
  });
143
143
  return;
144
144
  }
@@ -29,14 +29,14 @@ export type SchedulerActionResult =
29
29
  };
30
30
 
31
31
  export function sortItems(items: ScheduledItem[]): ScheduledItem[] {
32
- return [...items].sort((a, b) => {
33
- const aDate = typeof a.props.date === "string" ? a.props.date : null;
34
- const bDate = typeof b.props.date === "string" ? b.props.date : null;
35
- const aTime = typeof a.props.time === "string" ? a.props.time : "00:00";
36
- const bTime = typeof b.props.time === "string" ? b.props.time : "00:00";
37
- const aKey = aDate ? `0_${aDate}_${aTime}` : `1_${a.createdAt}`;
38
- const bKey = bDate ? `0_${bDate}_${bTime}` : `1_${b.createdAt}`;
39
- return aKey < bKey ? -1 : aKey > bKey ? 1 : 0;
32
+ return [...items].sort((left, right) => {
33
+ const leftDate = typeof left.props.date === "string" ? left.props.date : null;
34
+ const rightDate = typeof right.props.date === "string" ? right.props.date : null;
35
+ const leftTime = typeof left.props.time === "string" ? left.props.time : "00:00";
36
+ const rightTime = typeof right.props.time === "string" ? right.props.time : "00:00";
37
+ const leftKey = leftDate ? `0_${leftDate}_${leftTime}` : `1_${left.createdAt}`;
38
+ const rightKey = rightDate ? `0_${rightDate}_${rightTime}` : `1_${right.createdAt}`;
39
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
40
40
  });
41
41
  }
42
42
 
@@ -84,11 +84,11 @@ export function handleDelete(items: ScheduledItem[], input: SchedulerActionInput
84
84
 
85
85
  function applyPropPatch(current: ScheduledItem["props"], patch: Record<string, string | number | boolean | null>): ScheduledItem["props"] {
86
86
  const next: ScheduledItem["props"] = { ...current };
87
- for (const [k, v] of Object.entries(patch)) {
88
- if (v === null) {
89
- delete next[k];
87
+ for (const [key, value] of Object.entries(patch)) {
88
+ if (value === null) {
89
+ delete next[key];
90
90
  } else {
91
- next[k] = v;
91
+ next[key] = value;
92
92
  }
93
93
  }
94
94
  return next;
@@ -29,7 +29,7 @@ router.get(API_ROUTES.scheduler.tasks, (_req: Request, res: Response) => {
29
29
  // have no origin field of their own.
30
30
  const systemTasks = getSchedulerTasks();
31
31
  const userTasks = loadUserTasks();
32
- const all = [...systemTasks.map((t) => ({ ...t, origin: "system" as const })), ...userTasks.map((t) => ({ ...t, origin: "user" as const }))];
32
+ const all = [...systemTasks.map((task) => ({ ...task, origin: "system" as const })), ...userTasks.map((task) => ({ ...task, origin: "user" as const }))];
33
33
  res.json({ tasks: all });
34
34
  });
35
35
 
@@ -58,14 +58,14 @@ router.post(API_ROUTES.scheduler.tasks, async (req: Request, res: Response) => {
58
58
  // ── Update user task ────────────────────────────────────────────
59
59
 
60
60
  router.put(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, res: Response) => {
61
- const { id } = req.params;
61
+ const { id: taskId } = req.params;
62
62
  try {
63
63
  const updated = await withUserTaskLock(async (tasks) => {
64
- const result = applyUpdate(tasks, id, req.body);
64
+ const result = applyUpdate(tasks, taskId, req.body);
65
65
  if (result.kind === "error") {
66
66
  throw new Error(result.error);
67
67
  }
68
- const task = result.tasks.find((t) => t.id === id);
68
+ const task = result.tasks.find((taskItem) => taskItem.id === taskId);
69
69
  return { tasks: result.tasks, result: task };
70
70
  });
71
71
  res.json({ task: updated });
@@ -83,15 +83,15 @@ router.put(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, res:
83
83
  // ── Delete user task ────────────────────────────────────────────
84
84
 
85
85
  router.delete(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, res: Response) => {
86
- const { id } = req.params;
86
+ const { id: taskId } = req.params;
87
87
  try {
88
88
  await withUserTaskLock(async (tasks) => {
89
- const idx = tasks.findIndex((t) => t.id === id);
90
- if (idx === -1) throw new Error(`task not found: ${id}`);
91
- const next = tasks.filter((t) => t.id !== id);
89
+ const index = tasks.findIndex((task) => task.id === taskId);
90
+ if (index === -1) throw new Error(`task not found: ${taskId}`);
91
+ const next = tasks.filter((task) => task.id !== taskId);
92
92
  return { tasks: next, result: undefined };
93
93
  });
94
- res.json({ deleted: id });
94
+ res.json({ deleted: taskId });
95
95
  } catch (err) {
96
96
  const msg = errorMessage(err);
97
97
  if (msg.startsWith("task not found")) {
@@ -106,10 +106,10 @@ router.delete(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, re
106
106
  // ── Manual trigger ──────────────────────────────────────────────
107
107
 
108
108
  router.post(API_ROUTES.scheduler.taskRun, async (req: Request<{ id: string }>, res: Response) => {
109
- const { id } = req.params;
109
+ const { id: taskId } = req.params;
110
110
  // Check user tasks first
111
111
  const userTasks = loadUserTasks();
112
- const userTask = userTasks.find((t) => t.id === id);
112
+ const userTask = userTasks.find((task) => task.id === taskId);
113
113
  if (userTask) {
114
114
  const chatSessionId = crypto.randomUUID();
115
115
  log.info("scheduler-tasks", "manual run (user task)", {
@@ -126,14 +126,14 @@ router.post(API_ROUTES.scheduler.taskRun, async (req: Request<{ id: string }>, r
126
126
  error: String(err),
127
127
  });
128
128
  });
129
- res.json({ triggered: id, chatSessionId });
129
+ res.json({ triggered: taskId, chatSessionId });
130
130
  return;
131
131
  }
132
132
  // Not a user task — check system/skill tasks
133
133
  const systemTasks = getSchedulerTasks();
134
- const found = systemTasks.find((t) => t.id === id);
134
+ const found = systemTasks.find((task) => task.id === taskId);
135
135
  if (!found) {
136
- notFound(res, `task not found: ${id}`);
136
+ notFound(res, `task not found: ${taskId}`);
137
137
  return;
138
138
  }
139
139
  // System tasks don't have a prompt to startChat with — return 400