mulmoclaude 0.6.1 → 0.6.2

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 (139) hide show
  1. package/bin/mulmoclaude.js +1 -1
  2. package/client/assets/PluginScopedRoot-YjvQq0Nn.js +3 -0
  3. package/client/assets/{html2canvas-CDGcmOD3-BbPeutDg.js → html2canvas-CDGcmOD3-Bkf2uOth.js} +1 -1
  4. package/client/assets/{index-BZdOOa5E.js → index-BwrlMMHr.js} +66 -65
  5. package/client/assets/index-CvvNuegU.css +2 -0
  6. package/client/assets/{index.es-DqtpmBm8-DJdTPdnc.js → index.es-DqtpmBm8-D9mAh_KQ.js} +1 -1
  7. package/client/assets/material-symbols-outlined-BOZVWuR3.woff2 +0 -0
  8. package/client/assets/runtime-protocol-vue-C1To4M3t.js +1 -0
  9. package/client/index.html +7 -6
  10. package/package.json +7 -7
  11. package/server/accounting/eventPublisher.ts +2 -1
  12. package/server/accounting/snapshotCache.ts +2 -1
  13. package/server/agent/backend/claude-code.ts +1 -0
  14. package/server/agent/backend/types.ts +3 -0
  15. package/server/agent/config.ts +25 -2
  16. package/server/agent/index.ts +6 -0
  17. package/server/agent/prompt.ts +37 -24
  18. package/server/api/routes/accounting.ts +31 -24
  19. package/server/api/routes/agent.ts +2 -2
  20. package/server/api/routes/config-refresh.ts +49 -0
  21. package/server/api/routes/config.ts +77 -67
  22. package/server/api/routes/files.ts +41 -17
  23. package/server/api/routes/hookLog.ts +95 -0
  24. package/server/api/routes/news.ts +39 -52
  25. package/server/api/routes/notifier.ts +14 -19
  26. package/server/api/routes/pdf.ts +2 -2
  27. package/server/api/routes/presentSvg.ts +107 -0
  28. package/server/api/routes/scheduler.ts +100 -98
  29. package/server/api/routes/schedulerTasks.ts +98 -95
  30. package/server/api/routes/sessions.ts +22 -27
  31. package/server/api/routes/sources.ts +45 -43
  32. package/server/api/routes/wiki/history.ts +6 -15
  33. package/server/api/routes/wiki.ts +73 -276
  34. package/server/events/file-change.ts +3 -2
  35. package/server/events/session-store/index.ts +2 -1
  36. package/server/index.ts +117 -8
  37. package/server/notifier/store.ts +3 -3
  38. package/server/plugins/preset-list.ts +16 -5
  39. package/server/plugins/runtime.ts +2 -2
  40. package/server/system/config.ts +44 -2
  41. package/server/utils/asyncHandler.ts +75 -0
  42. package/server/utils/files/accounting-io.ts +19 -20
  43. package/server/utils/files/journal-io.ts +2 -1
  44. package/server/utils/files/json.ts +8 -1
  45. package/server/utils/files/reference-dirs-io.ts +2 -3
  46. package/server/utils/files/scheduler-overrides-io.ts +2 -3
  47. package/server/utils/files/svg-store.ts +27 -0
  48. package/server/utils/files/user-tasks-io.ts +2 -3
  49. package/server/utils/regex.ts +3 -12
  50. package/server/utils/text.ts +29 -0
  51. package/server/workspace/chat-index/summarizer.ts +5 -3
  52. package/server/workspace/cooking-recipes/migrate.ts +125 -0
  53. package/server/workspace/custom-dirs.ts +2 -2
  54. package/server/workspace/hooks/dispatcher.mjs +300 -0
  55. package/server/workspace/hooks/dispatcher.ts +55 -0
  56. package/server/workspace/hooks/handlers/configRefresh.ts +38 -0
  57. package/server/workspace/hooks/handlers/skillBridge.ts +223 -0
  58. package/server/workspace/hooks/handlers/wikiSnapshot.ts +43 -0
  59. package/server/workspace/hooks/provision.ts +222 -0
  60. package/server/workspace/hooks/shared/sidecar.ts +124 -0
  61. package/server/workspace/hooks/shared/stdin.ts +60 -0
  62. package/server/workspace/hooks/shared/workspace.ts +13 -0
  63. package/server/workspace/journal/dailyPass.ts +1 -6
  64. package/server/workspace/memory/io.ts +1 -34
  65. package/server/workspace/memory/migrate.ts +2 -1
  66. package/server/workspace/memory/snapshot.ts +26 -0
  67. package/server/workspace/memory/topic-io.ts +1 -18
  68. package/server/workspace/paths.ts +10 -0
  69. package/server/workspace/skills-preset/mc-cooking-coach/SKILL.md +217 -0
  70. package/server/workspace/skills-preset/mc-manage-automations/SKILL.md +119 -0
  71. package/server/workspace/skills-preset/mc-manage-skills/SKILL.md +128 -0
  72. package/server/workspace/skills-preset/mc-manage-sources/SKILL.md +106 -0
  73. package/server/workspace/skills-preset.ts +2 -1
  74. package/server/workspace/wiki-pages/io.ts +2 -1
  75. package/src/App.vue +51 -3
  76. package/src/components/ChatInput.vue +7 -8
  77. package/src/components/FileContentHeader.vue +1 -6
  78. package/src/components/FileDropOverlay.vue +18 -0
  79. package/src/components/RolesView.vue +14 -5
  80. package/src/components/SettingsMcpTab.vue +15 -10
  81. package/src/components/SettingsModal.vue +116 -130
  82. package/src/components/SettingsModelTab.vue +121 -0
  83. package/src/composables/useContentDisplay.ts +16 -0
  84. package/src/composables/useFileDropZone.ts +148 -0
  85. package/src/composables/useSkillsList.ts +2 -1
  86. package/src/config/apiRoutes.ts +22 -0
  87. package/src/config/roles.ts +78 -48
  88. package/src/config/toolNames.ts +4 -1
  89. package/src/lang/de.ts +36 -1
  90. package/src/lang/en.ts +36 -1
  91. package/src/lang/es.ts +36 -1
  92. package/src/lang/fr.ts +36 -1
  93. package/src/lang/ja.ts +36 -1
  94. package/src/lang/ko.ts +36 -1
  95. package/src/lang/pt-BR.ts +36 -1
  96. package/src/lang/zh.ts +36 -1
  97. package/src/lib/wiki-page/index-parse.ts +221 -0
  98. package/src/lib/wiki-page/link.ts +62 -0
  99. package/src/lib/wiki-page/lint.ts +105 -0
  100. package/src/lib/wiki-page/paths.ts +35 -0
  101. package/src/lib/wiki-page/slug.ts +28 -40
  102. package/src/main.ts +1 -0
  103. package/src/plugins/_generated/metas.ts +2 -0
  104. package/src/plugins/_generated/registrations.ts +2 -0
  105. package/src/plugins/_generated/server-bindings.ts +3 -0
  106. package/src/plugins/accounting/Preview.vue +3 -6
  107. package/src/plugins/accounting/View.vue +2 -1
  108. package/src/plugins/accounting/components/AccountsModal.vue +3 -2
  109. package/src/plugins/accounting/components/JournalEntryForm.vue +2 -1
  110. package/src/plugins/accounting/components/JournalList.vue +2 -1
  111. package/src/plugins/accounting/components/OpeningBalancesForm.vue +2 -1
  112. package/src/plugins/accounting/currencies.ts +13 -0
  113. package/src/plugins/manageRoles/View.vue +16 -5
  114. package/src/plugins/photoLocations/View.vue +4 -2
  115. package/src/plugins/presentSVG/Preview.vue +56 -0
  116. package/src/plugins/presentSVG/View.vue +465 -0
  117. package/src/plugins/presentSVG/definition.ts +29 -0
  118. package/src/plugins/presentSVG/index.ts +49 -0
  119. package/src/plugins/presentSVG/meta.ts +14 -0
  120. package/src/plugins/scheduler/View.vue +3 -7
  121. package/src/plugins/skill/View.vue +11 -13
  122. package/src/plugins/wiki/View.vue +1 -1
  123. package/src/plugins/wiki/helpers.ts +23 -5
  124. package/src/plugins/wiki/route.ts +12 -11
  125. package/src/tools/runtimeLoader.ts +75 -9
  126. package/src/utils/format/bytes.ts +41 -0
  127. package/src/utils/format/date.ts +14 -2
  128. package/src/utils/markdown/setup.ts +5 -0
  129. package/src/utils/markdown/workspaceLinkify.ts +73 -0
  130. package/client/assets/index-Bl3vqgA6.css +0 -2
  131. package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
  132. package/client/assets/runtime-protocol-vue-6WYa8hAs.js +0 -1
  133. package/server/workspace/wiki-history/hook/snapshot.mjs +0 -98
  134. package/server/workspace/wiki-history/hook/snapshot.ts +0 -135
  135. package/server/workspace/wiki-history/provision.ts +0 -181
  136. /package/client/assets/{chunk-D8eiyYIV-C1eAZMzz.js → chunk-D8eiyYIV-CAXpUwLd.js} +0 -0
  137. /package/client/assets/{purify.es-Fx1Nqyry-BSVNht6S.js → purify.es-Fx1Nqyry-Dwtk-9WZ.js} +0 -0
  138. /package/client/assets/{typeof-DBp4T-Ny-C2xoZtcz.js → typeof-DBp4T-Ny-CSr8wx1e.js} +0 -0
  139. /package/client/assets/{vue-1e_vz2LW.js → vue-C8UuIO9J.js} +0 -0
@@ -10,8 +10,8 @@ import { saveUserTasks } from "../../utils/files/user-tasks-io.js";
10
10
  import { startChat } from "./agent.js";
11
11
  import { log } from "../../system/logger/index.js";
12
12
  import { SCHEDULER_ACTIONS, TASK_ACTIONS } from "../../../src/plugins/scheduler/actions.js";
13
- import { badRequest, notFound, serverError } from "../../utils/httpError.js";
14
- import { errorMessage } from "../../utils/errors.js";
13
+ import { badRequest, notFound } from "../../utils/httpError.js";
14
+ import { asyncHandler } from "../../utils/asyncHandler.js";
15
15
  import { makeUuid } from "../../utils/id.js";
16
16
 
17
17
  const router = Router();
@@ -47,118 +47,120 @@ interface SchedulerBody extends SchedulerActionInput {
47
47
  bindRoute(
48
48
  router,
49
49
  API_ROUTES.scheduler.dispatch,
50
- async (req: Request<object, unknown, SchedulerBody>, res: Response<DispatchSuccessResponse<ScheduledItem> | DispatchErrorResponse | unknown>) => {
51
- const { action, ...input } = req.body;
50
+ asyncHandler<Request<object, unknown, SchedulerBody>, Response<DispatchSuccessResponse<ScheduledItem> | DispatchErrorResponse | unknown>>(
51
+ "scheduler",
52
+ "Internal server error",
53
+ async (req, res) => {
54
+ const { action, ...input } = req.body;
52
55
 
53
- // Route task actions to the user-task subsystem
54
- if (TASK_ACTIONS.has(action)) {
55
- await handleTaskAction(action, input, res);
56
- return;
57
- }
56
+ // Route task actions to the user-task subsystem
57
+ if (TASK_ACTIONS.has(action)) {
58
+ await handleTaskAction(action, input, res);
59
+ return;
60
+ }
58
61
 
59
- // Calendar item actions (existing behavior)
60
- const items = loadItems();
61
- const result = dispatchScheduler(action, items, input);
62
- respondWithDispatchResult(res, result, {
63
- shouldPersist: action !== SCHEDULER_ACTIONS.show,
64
- instructions: "Display the updated scheduler to the user.",
65
- persist: saveItems,
66
- });
67
- },
62
+ // Calendar item actions (existing behavior)
63
+ const items = loadItems();
64
+ const result = dispatchScheduler(action, items, input);
65
+ respondWithDispatchResult(res, result, {
66
+ shouldPersist: action !== SCHEDULER_ACTIONS.show,
67
+ instructions: "Display the updated scheduler to the user.",
68
+ persist: saveItems,
69
+ });
70
+ },
71
+ ),
68
72
  );
69
73
 
70
74
  async function handleTaskAction(action: string, input: Record<string, unknown>, res: Response): Promise<void> {
71
75
  log.info("scheduler", "task action: start", { action });
72
- try {
73
- if (action === SCHEDULER_ACTIONS.listTasks) {
74
- const tasks = loadUserTasks();
75
- log.info("scheduler", "task action: listTasks ok", { tasks: tasks.length });
76
- res.json({
77
- uuid: makeUuid(),
78
- message: `${tasks.length} scheduled task(s) found.`,
79
- data: { tasks },
80
- });
81
- return;
82
- }
76
+ // Errors bubble up to the asyncHandler wrapper on the dispatch route,
77
+ // which logs at `log.error("scheduler", "handler threw", …)` and
78
+ // returns a generic 500. No inner try/catch needed here.
79
+ if (action === SCHEDULER_ACTIONS.listTasks) {
80
+ const tasks = loadUserTasks();
81
+ log.info("scheduler", "task action: listTasks ok", { tasks: tasks.length });
82
+ res.json({
83
+ uuid: makeUuid(),
84
+ message: `${tasks.length} scheduled task(s) found.`,
85
+ data: { tasks },
86
+ });
87
+ return;
88
+ }
83
89
 
84
- if (action === SCHEDULER_ACTIONS.createTask) {
85
- const result = validateAndCreate(input);
86
- if (result.kind === "error") {
87
- log.warn("scheduler", "task action: createTask validation failed", { error: result.error });
88
- badRequest(res, result.error);
89
- return;
90
- }
91
- const tasks = loadUserTasks();
92
- tasks.push(result.task);
93
- await saveUserTasks(tasks);
94
- await refreshUserTasks();
95
- log.info("scheduler", "task action: createTask ok", { id: result.task.id, name: result.task.name });
96
- res.json({
97
- uuid: makeUuid(),
98
- message: `Task "${result.task.name}" created and scheduled.`,
99
- data: { task: result.task },
100
- });
90
+ if (action === SCHEDULER_ACTIONS.createTask) {
91
+ const result = validateAndCreate(input);
92
+ if (result.kind === "error") {
93
+ log.warn("scheduler", "task action: createTask validation failed", { error: result.error });
94
+ badRequest(res, result.error);
101
95
  return;
102
96
  }
97
+ const tasks = loadUserTasks();
98
+ tasks.push(result.task);
99
+ await saveUserTasks(tasks);
100
+ await refreshUserTasks();
101
+ log.info("scheduler", "task action: createTask ok", { id: result.task.id, name: result.task.name });
102
+ res.json({
103
+ uuid: makeUuid(),
104
+ message: `Task "${result.task.name}" created and scheduled.`,
105
+ data: { task: result.task },
106
+ });
107
+ return;
108
+ }
103
109
 
104
- if (action === SCHEDULER_ACTIONS.deleteTask) {
105
- const taskId = typeof input.id === "string" ? input.id : "";
106
- const tasks = loadUserTasks();
107
- const idx = tasks.findIndex((task) => task.id === taskId);
108
- if (idx === -1) {
109
- log.warn("scheduler", "task action: deleteTask not found", { taskId });
110
- notFound(res, `task not found: ${taskId}`);
111
- return;
112
- }
113
- const { name } = tasks[idx];
114
- tasks.splice(idx, 1);
115
- await saveUserTasks(tasks);
116
- await refreshUserTasks();
117
- log.info("scheduler", "task action: deleteTask ok", { taskId, name });
118
- res.json({
119
- uuid: makeUuid(),
120
- message: `Task "${name}" deleted.`,
121
- data: { deleted: taskId },
122
- });
110
+ if (action === SCHEDULER_ACTIONS.deleteTask) {
111
+ const taskId = typeof input.id === "string" ? input.id : "";
112
+ const tasks = loadUserTasks();
113
+ const idx = tasks.findIndex((task) => task.id === taskId);
114
+ if (idx === -1) {
115
+ log.warn("scheduler", "task action: deleteTask not found", { taskId });
116
+ notFound(res, `task not found: ${taskId}`);
123
117
  return;
124
118
  }
119
+ const { name } = tasks[idx];
120
+ tasks.splice(idx, 1);
121
+ await saveUserTasks(tasks);
122
+ await refreshUserTasks();
123
+ log.info("scheduler", "task action: deleteTask ok", { taskId, name });
124
+ res.json({
125
+ uuid: makeUuid(),
126
+ message: `Task "${name}" deleted.`,
127
+ data: { deleted: taskId },
128
+ });
129
+ return;
130
+ }
125
131
 
126
- if (action === SCHEDULER_ACTIONS.runTask) {
127
- const taskId = typeof input.id === "string" ? input.id : "";
128
- const tasks = loadUserTasks();
129
- const task = tasks.find((candidate) => candidate.id === taskId);
130
- if (!task) {
131
- notFound(res, `task not found: ${taskId}`);
132
- return;
133
- }
134
- const chatSessionId = makeUuid();
135
- log.info("scheduler", "manual run via MCP", {
136
- name: task.name,
137
- chatSessionId,
138
- });
139
- startChat({
140
- message: task.prompt,
141
- roleId: task.roleId,
142
- chatSessionId,
143
- origin: SESSION_ORIGINS.scheduler,
144
- }).catch((err) => {
145
- log.error("scheduler", "manual run failed", {
146
- error: String(err),
147
- });
148
- });
149
- res.json({
150
- uuid: makeUuid(),
151
- message: `Task "${task.name}" triggered.`,
152
- data: { triggered: taskId, chatSessionId },
153
- });
132
+ if (action === SCHEDULER_ACTIONS.runTask) {
133
+ const taskId = typeof input.id === "string" ? input.id : "";
134
+ const tasks = loadUserTasks();
135
+ const task = tasks.find((candidate) => candidate.id === taskId);
136
+ if (!task) {
137
+ notFound(res, `task not found: ${taskId}`);
154
138
  return;
155
139
  }
156
-
157
- badRequest(res, `unknown task action: ${action}`);
158
- } catch (err) {
159
- log.error("scheduler", "task action failed", { error: errorMessage(err) });
160
- serverError(res, "Internal server error");
140
+ const chatSessionId = makeUuid();
141
+ log.info("scheduler", "manual run via MCP", {
142
+ name: task.name,
143
+ chatSessionId,
144
+ });
145
+ startChat({
146
+ message: task.prompt,
147
+ roleId: task.roleId,
148
+ chatSessionId,
149
+ origin: SESSION_ORIGINS.scheduler,
150
+ }).catch((err) => {
151
+ log.error("scheduler", "manual run failed", {
152
+ error: String(err),
153
+ });
154
+ });
155
+ res.json({
156
+ uuid: makeUuid(),
157
+ message: `Task "${task.name}" triggered.`,
158
+ data: { triggered: taskId, chatSessionId },
159
+ });
160
+ return;
161
161
  }
162
+
163
+ badRequest(res, `unknown task action: ${action}`);
162
164
  }
163
165
 
164
166
  export default router;
@@ -14,20 +14,23 @@ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
14
14
  import { bindRoute } from "../../utils/router.js";
15
15
  import { SESSION_ORIGINS } from "../../../src/types/session.js";
16
16
  import { loadUserTasks, validateAndCreate, applyUpdate, withUserTaskLock } from "../../workspace/skills/user-tasks.js";
17
- import { badRequest, notFound, serverError } from "../../utils/httpError.js";
17
+ import { badRequest, notFound } from "../../utils/httpError.js";
18
18
  import { errorMessage } from "../../utils/errors.js";
19
19
  import { getOptionalStringQuery } from "../../utils/request.js";
20
20
  import { log } from "../../system/logger/index.js";
21
21
  import { startChat } from "./agent.js";
22
22
  import { makeUuid } from "../../utils/id.js";
23
+ import { asyncHandler } from "../../utils/asyncHandler.js";
23
24
 
24
25
  const router = Router();
25
26
 
26
27
  // ── List all tasks ──────────────────────────────────────────────
27
28
 
28
- bindRoute(router, API_ROUTES.scheduler.tasksList, (_req: Request, res: Response) => {
29
- log.info("scheduler-tasks", "list: start");
30
- try {
29
+ bindRoute(
30
+ router,
31
+ API_ROUTES.scheduler.tasksList,
32
+ asyncHandler("scheduler-tasks", "Failed to list tasks", async (_req, res) => {
33
+ log.info("scheduler-tasks", "list: start");
31
34
  // getSchedulerTasks() returns system-only tasks (registered via
32
35
  // initScheduler at startup — journal, chat-index, sources, etc.).
33
36
  // origin: "system" is correct, not an overwrite — these tasks
@@ -37,95 +40,92 @@ bindRoute(router, API_ROUTES.scheduler.tasksList, (_req: Request, res: Response)
37
40
  const all = [...systemTasks.map((task) => ({ ...task, origin: "system" as const })), ...userTasks.map((task) => ({ ...task, origin: "user" as const }))];
38
41
  log.info("scheduler-tasks", "list: ok", { system: systemTasks.length, user: userTasks.length });
39
42
  res.json({ tasks: all });
40
- } catch (err) {
41
- // loadUserTasks reads JSON from disk and getSchedulerTasks
42
- // queries the in-memory registry — neither is supposed to
43
- // throw, but a corrupted user-tasks.json or an early-startup
44
- // registry race would. Without the catch the route would 500
45
- // with no trace, which is the original #779 complaint pattern.
46
- log.error("scheduler-tasks", "list: failed", { error: errorMessage(err) });
47
- serverError(res, "Failed to list tasks");
48
- }
49
- });
43
+ }),
44
+ );
50
45
 
51
46
  // ── Create user task ────────────────────────────────────────────
52
47
 
53
- bindRoute(router, API_ROUTES.scheduler.tasksCreate, async (req: Request, res: Response) => {
54
- log.info("scheduler-tasks", "create: start");
55
- const validated = validateAndCreate(req.body);
56
- if (validated.kind === "error") {
57
- log.warn("scheduler-tasks", "create: validation failed", { error: validated.error });
58
- badRequest(res, validated.error);
59
- return;
60
- }
61
- try {
48
+ bindRoute(
49
+ router,
50
+ API_ROUTES.scheduler.tasksCreate,
51
+ asyncHandler("scheduler-tasks", "Failed to create task", async (req, res) => {
52
+ log.info("scheduler-tasks", "create: start");
53
+ const validated = validateAndCreate(req.body);
54
+ if (validated.kind === "error") {
55
+ log.warn("scheduler-tasks", "create: validation failed", { error: validated.error });
56
+ badRequest(res, validated.error);
57
+ return;
58
+ }
62
59
  const task = await withUserTaskLock(async (tasks) => ({
63
60
  tasks: [...tasks, validated.task],
64
61
  result: validated.task,
65
62
  }));
66
63
  log.info("scheduler-tasks", "create: ok", { id: task.id, name: task.name });
67
64
  res.status(201).json({ task });
68
- } catch (err) {
69
- log.error("scheduler-tasks", "create: failed", {
70
- error: String(err),
71
- });
72
- serverError(res, "Failed to create task");
73
- }
74
- });
65
+ }),
66
+ );
75
67
 
76
68
  // ── Update user task ────────────────────────────────────────────
77
69
 
78
- bindRoute(router, API_ROUTES.scheduler.taskUpdate, async (req: Request<{ id: string }>, res: Response) => {
79
- const { id: taskId } = req.params;
80
- log.info("scheduler-tasks", "update: start", { taskId });
81
- try {
82
- const updated = await withUserTaskLock(async (tasks) => {
83
- const result = applyUpdate(tasks, taskId, req.body);
84
- if (result.kind === "error") {
85
- throw new Error(result.error);
70
+ bindRoute(
71
+ router,
72
+ API_ROUTES.scheduler.taskUpdate,
73
+ asyncHandler<Request<{ id: string }>, Response>("scheduler-tasks", "Failed to update task", async (req, res) => {
74
+ const { id: taskId } = req.params;
75
+ log.info("scheduler-tasks", "update: start", { taskId });
76
+ try {
77
+ const updated = await withUserTaskLock(async (tasks) => {
78
+ const result = applyUpdate(tasks, taskId, req.body);
79
+ if (result.kind === "error") {
80
+ throw new Error(result.error);
81
+ }
82
+ const task = result.tasks.find((taskItem) => taskItem.id === taskId);
83
+ return { tasks: result.tasks, result: task };
84
+ });
85
+ log.info("scheduler-tasks", "update: ok", { taskId });
86
+ res.json({ task: updated });
87
+ } catch (err) {
88
+ // Domain-shaped errors → 404; everything else rethrows for the
89
+ // asyncHandler wrapper to surface as 500.
90
+ const msg = errorMessage(err);
91
+ if (msg.startsWith("task not found") || msg.startsWith("request body")) {
92
+ log.warn("scheduler-tasks", "update: validation failed", { taskId, reason: msg });
93
+ notFound(res, msg);
94
+ return;
86
95
  }
87
- const task = result.tasks.find((taskItem) => taskItem.id === taskId);
88
- return { tasks: result.tasks, result: task };
89
- });
90
- log.info("scheduler-tasks", "update: ok", { taskId });
91
- res.json({ task: updated });
92
- } catch (err) {
93
- const msg = errorMessage(err);
94
- if (msg.startsWith("task not found") || msg.startsWith("request body")) {
95
- log.warn("scheduler-tasks", "update: validation failed", { taskId, reason: msg });
96
- notFound(res, msg);
97
- return;
96
+ throw err;
98
97
  }
99
- log.error("scheduler-tasks", "update: failed", { taskId, error: msg });
100
- serverError(res, "Failed to update task");
101
- }
102
- });
98
+ }),
99
+ );
103
100
 
104
101
  // ── Delete user task ────────────────────────────────────────────
105
102
 
106
- bindRoute(router, API_ROUTES.scheduler.taskDelete, async (req: Request<{ id: string }>, res: Response) => {
107
- const { id: taskId } = req.params;
108
- log.info("scheduler-tasks", "delete: start", { taskId });
109
- try {
110
- await withUserTaskLock(async (tasks) => {
111
- const index = tasks.findIndex((task) => task.id === taskId);
112
- if (index === -1) throw new Error(`task not found: ${taskId}`);
113
- const next = tasks.filter((task) => task.id !== taskId);
114
- return { tasks: next, result: undefined };
115
- });
116
- log.info("scheduler-tasks", "delete: ok", { taskId });
117
- res.json({ deleted: taskId });
118
- } catch (err) {
119
- const msg = errorMessage(err);
120
- if (msg.startsWith("task not found")) {
121
- log.warn("scheduler-tasks", "delete: not found", { taskId });
122
- notFound(res, msg);
123
- return;
103
+ bindRoute(
104
+ router,
105
+ API_ROUTES.scheduler.taskDelete,
106
+ asyncHandler<Request<{ id: string }>, Response>("scheduler-tasks", "Failed to delete task", async (req, res) => {
107
+ const { id: taskId } = req.params;
108
+ log.info("scheduler-tasks", "delete: start", { taskId });
109
+ try {
110
+ await withUserTaskLock(async (tasks) => {
111
+ const index = tasks.findIndex((task) => task.id === taskId);
112
+ if (index === -1) throw new Error(`task not found: ${taskId}`);
113
+ const next = tasks.filter((task) => task.id !== taskId);
114
+ return { tasks: next, result: undefined };
115
+ });
116
+ log.info("scheduler-tasks", "delete: ok", { taskId });
117
+ res.json({ deleted: taskId });
118
+ } catch (err) {
119
+ const msg = errorMessage(err);
120
+ if (msg.startsWith("task not found")) {
121
+ log.warn("scheduler-tasks", "delete: not found", { taskId });
122
+ notFound(res, msg);
123
+ return;
124
+ }
125
+ throw err;
124
126
  }
125
- log.error("scheduler-tasks", "delete: failed", { taskId, error: msg });
126
- serverError(res, "Failed to delete task");
127
- }
128
- });
127
+ }),
128
+ );
129
129
 
130
130
  // ── Manual trigger ──────────────────────────────────────────────
131
131
 
@@ -177,25 +177,28 @@ interface LogQuery {
177
177
  limit?: string;
178
178
  }
179
179
 
180
- bindRoute(router, API_ROUTES.scheduler.logs, async (req: Request<object, unknown, object, LogQuery>, res: Response<{ logs: TaskLogEntry[] }>) => {
181
- const MAX_LIMIT = 500;
182
- const rawLimitStr = getOptionalStringQuery(req, "limit");
183
- const rawLimit = rawLimitStr ? parseInt(rawLimitStr, 10) : undefined;
184
- const limit = rawLimit !== undefined && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, MAX_LIMIT) : undefined;
185
- const taskId = getOptionalStringQuery(req, "taskId");
186
- log.info("scheduler-tasks", "logs: start", { taskId, limit });
187
- try {
188
- const logs = await getSchedulerLogs({
189
- since: getOptionalStringQuery(req, "since"),
190
- taskId,
191
- limit,
192
- });
193
- log.info("scheduler-tasks", "logs: ok", { entries: logs.length, taskId });
194
- res.json({ logs });
195
- } catch (err) {
196
- log.error("scheduler-tasks", "logs: failed", { taskId, error: errorMessage(err) });
197
- serverError(res, "Failed to read scheduler logs");
198
- }
199
- });
180
+ bindRoute(
181
+ router,
182
+ API_ROUTES.scheduler.logs,
183
+ asyncHandler<Request<object, unknown, object, LogQuery>, Response<{ logs: TaskLogEntry[] }>>(
184
+ "scheduler-tasks",
185
+ "Failed to read scheduler logs",
186
+ async (req, res) => {
187
+ const MAX_LIMIT = 500;
188
+ const rawLimitStr = getOptionalStringQuery(req, "limit");
189
+ const rawLimit = rawLimitStr ? parseInt(rawLimitStr, 10) : undefined;
190
+ const limit = rawLimit !== undefined && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, MAX_LIMIT) : undefined;
191
+ const taskId = getOptionalStringQuery(req, "taskId");
192
+ log.info("scheduler-tasks", "logs: start", { taskId, limit });
193
+ const logs = await getSchedulerLogs({
194
+ since: getOptionalStringQuery(req, "since"),
195
+ taskId,
196
+ limit,
197
+ });
198
+ log.info("scheduler-tasks", "logs: ok", { entries: logs.length, taskId });
199
+ res.json({ logs });
200
+ },
201
+ ),
202
+ );
200
203
 
201
204
  export default router;
@@ -25,6 +25,7 @@ import { ONE_DAY_MS } from "../../utils/time.js";
25
25
  import { encodeCursor, parseCursor, sessionChangeMs } from "./sessionsCursor.js";
26
26
  import { errorMessage } from "../../utils/errors.js";
27
27
  import { log } from "../../system/logger/index.js";
28
+ import { asyncHandler } from "../../utils/asyncHandler.js";
28
29
 
29
30
  interface SessionMeta {
30
31
  roleId: string;
@@ -355,14 +356,13 @@ router.post(API_ROUTES.sessions.markRead, async (req: Request<SessionIdParams>,
355
356
  // Toggle the user-set bookmark flag on a session's meta sidecar.
356
357
  router.post(
357
358
  API_ROUTES.sessions.bookmark,
358
- async (
359
- req: Request<SessionIdParams, { ok: boolean } | SessionErrorResponse, { bookmarked: boolean }>,
360
- res: Response<{ ok: boolean } | SessionErrorResponse>,
361
- ) => {
362
- const { id: sessionId } = req.params;
363
- const bookmarked = Boolean(req.body?.bookmarked);
364
- log.info("sessions", "bookmark: start", { sessionId, bookmarked });
365
- try {
359
+ asyncHandler<Request<SessionIdParams, { ok: boolean } | SessionErrorResponse, { bookmarked: boolean }>, Response<{ ok: boolean } | SessionErrorResponse>>(
360
+ "sessions",
361
+ "Failed to update bookmark",
362
+ async (req, res) => {
363
+ const { id: sessionId } = req.params;
364
+ const bookmarked = Boolean(req.body?.bookmarked);
365
+ log.info("sessions", "bookmark: start", { sessionId, bookmarked });
366
366
  await updateIsBookmarked(sessionId, bookmarked);
367
367
  // Meta-mtime bumps on the write — cursor diff will pick up the
368
368
  // change on the next refetch — but every other tab also needs
@@ -370,11 +370,8 @@ router.post(
370
370
  publishSessionsChanged();
371
371
  log.info("sessions", "bookmark: ok", { sessionId, bookmarked });
372
372
  res.json({ ok: true });
373
- } catch (err) {
374
- log.error("sessions", "bookmark: threw", { sessionId, error: errorMessage(err) });
375
- res.status(500).json({ error: "Failed to update bookmark" });
376
- }
377
- },
373
+ },
374
+ ),
378
375
  );
379
376
 
380
377
  // Hard-delete a session: remove the jsonl, meta sidecar, AND the
@@ -395,24 +392,22 @@ router.post(
395
392
  // 3. Only after disk is clean do we evict from the store and fire
396
393
  // `notifySessionsChanged({ deletedIds })`. Now the broadcast is
397
394
  // a truthful statement.
398
- router.delete(API_ROUTES.sessions.detail, async (req: Request<SessionIdParams>, res: Response<{ ok: boolean } | SessionErrorResponse>) => {
399
- const { id: sessionId } = req.params;
400
- log.info("sessions", "delete: start", { sessionId });
401
- if (getSession(sessionId)?.isRunning) {
402
- log.warn("sessions", "delete: refused — session running", { sessionId });
403
- res.status(409).json({ error: "Session is running. Cancel the run before deleting." });
404
- return;
405
- }
406
- try {
395
+ router.delete(
396
+ API_ROUTES.sessions.detail,
397
+ asyncHandler<Request<SessionIdParams>, Response<{ ok: boolean } | SessionErrorResponse>>("sessions", "Failed to delete session", async (req, res) => {
398
+ const { id: sessionId } = req.params;
399
+ log.info("sessions", "delete: start", { sessionId });
400
+ if (getSession(sessionId)?.isRunning) {
401
+ log.warn("sessions", "delete: refused — session running", { sessionId });
402
+ res.status(409).json({ error: "Session is running. Cancel the run before deleting." });
403
+ return;
404
+ }
407
405
  await deleteSessionFiles(sessionId);
408
406
  await removeSessionFromIndex(workspacePath, sessionId);
409
407
  evictSession(sessionId);
410
408
  log.info("sessions", "delete: ok", { sessionId });
411
409
  res.json({ ok: true });
412
- } catch (err) {
413
- log.error("sessions", "delete: threw", { sessionId, error: errorMessage(err) });
414
- res.status(500).json({ error: "Failed to delete session" });
415
- }
416
- });
410
+ }),
411
+ );
417
412
 
418
413
  export default router;