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.
- package/bin/mulmoclaude.js +1 -1
- package/client/assets/PluginScopedRoot-YjvQq0Nn.js +3 -0
- package/client/assets/{html2canvas-CDGcmOD3-BbPeutDg.js → html2canvas-CDGcmOD3-Bkf2uOth.js} +1 -1
- package/client/assets/{index-BZdOOa5E.js → index-BwrlMMHr.js} +66 -65
- package/client/assets/index-CvvNuegU.css +2 -0
- package/client/assets/{index.es-DqtpmBm8-DJdTPdnc.js → index.es-DqtpmBm8-D9mAh_KQ.js} +1 -1
- package/client/assets/material-symbols-outlined-BOZVWuR3.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-C1To4M3t.js +1 -0
- package/client/index.html +7 -6
- package/package.json +7 -7
- package/server/accounting/eventPublisher.ts +2 -1
- package/server/accounting/snapshotCache.ts +2 -1
- package/server/agent/backend/claude-code.ts +1 -0
- package/server/agent/backend/types.ts +3 -0
- package/server/agent/config.ts +25 -2
- package/server/agent/index.ts +6 -0
- package/server/agent/prompt.ts +37 -24
- package/server/api/routes/accounting.ts +31 -24
- package/server/api/routes/agent.ts +2 -2
- package/server/api/routes/config-refresh.ts +49 -0
- package/server/api/routes/config.ts +77 -67
- package/server/api/routes/files.ts +41 -17
- package/server/api/routes/hookLog.ts +95 -0
- package/server/api/routes/news.ts +39 -52
- package/server/api/routes/notifier.ts +14 -19
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/presentSvg.ts +107 -0
- package/server/api/routes/scheduler.ts +100 -98
- package/server/api/routes/schedulerTasks.ts +98 -95
- package/server/api/routes/sessions.ts +22 -27
- package/server/api/routes/sources.ts +45 -43
- package/server/api/routes/wiki/history.ts +6 -15
- package/server/api/routes/wiki.ts +73 -276
- package/server/events/file-change.ts +3 -2
- package/server/events/session-store/index.ts +2 -1
- package/server/index.ts +117 -8
- package/server/notifier/store.ts +3 -3
- package/server/plugins/preset-list.ts +16 -5
- package/server/plugins/runtime.ts +2 -2
- package/server/system/config.ts +44 -2
- package/server/utils/asyncHandler.ts +75 -0
- package/server/utils/files/accounting-io.ts +19 -20
- package/server/utils/files/journal-io.ts +2 -1
- package/server/utils/files/json.ts +8 -1
- package/server/utils/files/reference-dirs-io.ts +2 -3
- package/server/utils/files/scheduler-overrides-io.ts +2 -3
- package/server/utils/files/svg-store.ts +27 -0
- package/server/utils/files/user-tasks-io.ts +2 -3
- package/server/utils/regex.ts +3 -12
- package/server/utils/text.ts +29 -0
- package/server/workspace/chat-index/summarizer.ts +5 -3
- package/server/workspace/cooking-recipes/migrate.ts +125 -0
- package/server/workspace/custom-dirs.ts +2 -2
- package/server/workspace/hooks/dispatcher.mjs +300 -0
- package/server/workspace/hooks/dispatcher.ts +55 -0
- package/server/workspace/hooks/handlers/configRefresh.ts +38 -0
- package/server/workspace/hooks/handlers/skillBridge.ts +223 -0
- package/server/workspace/hooks/handlers/wikiSnapshot.ts +43 -0
- package/server/workspace/hooks/provision.ts +222 -0
- package/server/workspace/hooks/shared/sidecar.ts +124 -0
- package/server/workspace/hooks/shared/stdin.ts +60 -0
- package/server/workspace/hooks/shared/workspace.ts +13 -0
- package/server/workspace/journal/dailyPass.ts +1 -6
- package/server/workspace/memory/io.ts +1 -34
- package/server/workspace/memory/migrate.ts +2 -1
- package/server/workspace/memory/snapshot.ts +26 -0
- package/server/workspace/memory/topic-io.ts +1 -18
- package/server/workspace/paths.ts +10 -0
- package/server/workspace/skills-preset/mc-cooking-coach/SKILL.md +217 -0
- package/server/workspace/skills-preset/mc-manage-automations/SKILL.md +119 -0
- package/server/workspace/skills-preset/mc-manage-skills/SKILL.md +128 -0
- package/server/workspace/skills-preset/mc-manage-sources/SKILL.md +106 -0
- package/server/workspace/skills-preset.ts +2 -1
- package/server/workspace/wiki-pages/io.ts +2 -1
- package/src/App.vue +51 -3
- package/src/components/ChatInput.vue +7 -8
- package/src/components/FileContentHeader.vue +1 -6
- package/src/components/FileDropOverlay.vue +18 -0
- package/src/components/RolesView.vue +14 -5
- package/src/components/SettingsMcpTab.vue +15 -10
- package/src/components/SettingsModal.vue +116 -130
- package/src/components/SettingsModelTab.vue +121 -0
- package/src/composables/useContentDisplay.ts +16 -0
- package/src/composables/useFileDropZone.ts +148 -0
- package/src/composables/useSkillsList.ts +2 -1
- package/src/config/apiRoutes.ts +22 -0
- package/src/config/roles.ts +78 -48
- package/src/config/toolNames.ts +4 -1
- package/src/lang/de.ts +36 -1
- package/src/lang/en.ts +36 -1
- package/src/lang/es.ts +36 -1
- package/src/lang/fr.ts +36 -1
- package/src/lang/ja.ts +36 -1
- package/src/lang/ko.ts +36 -1
- package/src/lang/pt-BR.ts +36 -1
- package/src/lang/zh.ts +36 -1
- package/src/lib/wiki-page/index-parse.ts +221 -0
- package/src/lib/wiki-page/link.ts +62 -0
- package/src/lib/wiki-page/lint.ts +105 -0
- package/src/lib/wiki-page/paths.ts +35 -0
- package/src/lib/wiki-page/slug.ts +28 -40
- package/src/main.ts +1 -0
- package/src/plugins/_generated/metas.ts +2 -0
- package/src/plugins/_generated/registrations.ts +2 -0
- package/src/plugins/_generated/server-bindings.ts +3 -0
- package/src/plugins/accounting/Preview.vue +3 -6
- package/src/plugins/accounting/View.vue +2 -1
- package/src/plugins/accounting/components/AccountsModal.vue +3 -2
- package/src/plugins/accounting/components/JournalEntryForm.vue +2 -1
- package/src/plugins/accounting/components/JournalList.vue +2 -1
- package/src/plugins/accounting/components/OpeningBalancesForm.vue +2 -1
- package/src/plugins/accounting/currencies.ts +13 -0
- package/src/plugins/manageRoles/View.vue +16 -5
- package/src/plugins/photoLocations/View.vue +4 -2
- package/src/plugins/presentSVG/Preview.vue +56 -0
- package/src/plugins/presentSVG/View.vue +465 -0
- package/src/plugins/presentSVG/definition.ts +29 -0
- package/src/plugins/presentSVG/index.ts +49 -0
- package/src/plugins/presentSVG/meta.ts +14 -0
- package/src/plugins/scheduler/View.vue +3 -7
- package/src/plugins/skill/View.vue +11 -13
- package/src/plugins/wiki/View.vue +1 -1
- package/src/plugins/wiki/helpers.ts +23 -5
- package/src/plugins/wiki/route.ts +12 -11
- package/src/tools/runtimeLoader.ts +75 -9
- package/src/utils/format/bytes.ts +41 -0
- package/src/utils/format/date.ts +14 -2
- package/src/utils/markdown/setup.ts +5 -0
- package/src/utils/markdown/workspaceLinkify.ts +73 -0
- package/client/assets/index-Bl3vqgA6.css +0 -2
- package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-6WYa8hAs.js +0 -1
- package/server/workspace/wiki-history/hook/snapshot.mjs +0 -98
- package/server/workspace/wiki-history/hook/snapshot.ts +0 -135
- package/server/workspace/wiki-history/provision.ts +0 -181
- /package/client/assets/{chunk-D8eiyYIV-C1eAZMzz.js → chunk-D8eiyYIV-CAXpUwLd.js} +0 -0
- /package/client/assets/{purify.es-Fx1Nqyry-BSVNht6S.js → purify.es-Fx1Nqyry-Dwtk-9WZ.js} +0 -0
- /package/client/assets/{typeof-DBp4T-Ny-C2xoZtcz.js → typeof-DBp4T-Ny-CSr8wx1e.js} +0 -0
- /package/client/assets/{vue-1e_vz2LW.js → vue-C8UuIO9J.js} +0 -0
package/server/agent/config.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import type { Role } from "../../src/config/roles.js";
|
|
5
5
|
import { mcpTools, isMcpToolEnabled } from "./mcp-tools/index.js";
|
|
6
6
|
import { getActiveToolDescriptors } from "./activeTools.js";
|
|
7
|
-
import type { McpServerSpec } from "../system/config.js";
|
|
7
|
+
import type { EffortLevel, McpServerSpec } from "../system/config.js";
|
|
8
8
|
import { getCurrentToken } from "../api/auth/token.js";
|
|
9
9
|
import type { Attachment } from "@mulmobridge/protocol";
|
|
10
10
|
import { isImageMime, isNativeAttachmentMime } from "@mulmobridge/client";
|
|
@@ -79,6 +79,22 @@ export function prepareUserServers(userServers: Record<string, McpServerSpec>, u
|
|
|
79
79
|
if (spec.type === "http") {
|
|
80
80
|
out[serverId] = prepareUserHttpServer(spec, useDocker);
|
|
81
81
|
} else {
|
|
82
|
+
// Stay symmetric with `userServerAllowedToolNames`: stdio
|
|
83
|
+
// servers can't run inside the sandbox image (see
|
|
84
|
+
// docs/mcp-sandbox.md for the full rationale — #162 / #1334).
|
|
85
|
+
// Claude CLI 2.1.x silently exits 1 when a stdio MCP fails to
|
|
86
|
+
// start, so passing the spec through here would mask the
|
|
87
|
+
// failure as a generic boot error. Drop + log per skipped
|
|
88
|
+
// entry so an operator scanning the log knows why their MCP
|
|
89
|
+
// didn't load.
|
|
90
|
+
if (useDocker) {
|
|
91
|
+
log.info("mcp", "skipping stdio server in Docker sandbox", {
|
|
92
|
+
serverId,
|
|
93
|
+
transport: "stdio",
|
|
94
|
+
reason: "sandbox image is too minimal to host arbitrary stdio MCP runtimes",
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
82
98
|
out[serverId] = prepareUserStdioServer(spec, useDocker, hostWorkspacePath);
|
|
83
99
|
}
|
|
84
100
|
}
|
|
@@ -211,10 +227,13 @@ export interface CliArgsParams {
|
|
|
211
227
|
// Web UI-managed extension of the allowed-tools list. Merged with
|
|
212
228
|
// BASE_ALLOWED_TOOLS and the mcp__mulmoclaude__ plugin names.
|
|
213
229
|
extraAllowedTools?: string[];
|
|
230
|
+
// Reasoning effort (#1323). When undefined, the flag is omitted
|
|
231
|
+
// and Claude picks its own default.
|
|
232
|
+
effortLevel?: EffortLevel;
|
|
214
233
|
}
|
|
215
234
|
|
|
216
235
|
export function buildCliArgs(params: CliArgsParams): string[] {
|
|
217
|
-
const { systemPrompt, activePlugins, claudeSessionId, mcpConfigPath, extraAllowedTools = [] } = params;
|
|
236
|
+
const { systemPrompt, activePlugins, claudeSessionId, mcpConfigPath, extraAllowedTools = [], effortLevel } = params;
|
|
218
237
|
|
|
219
238
|
const mcpToolNames = activePlugins.map((pluginName) => `mcp__mulmoclaude__${pluginName}`);
|
|
220
239
|
// DEBUG: also pass the wildcard form `mcp__mulmoclaude` so Claude
|
|
@@ -264,6 +283,10 @@ export function buildCliArgs(params: CliArgsParams): string[] {
|
|
|
264
283
|
args.push("--strict-mcp-config");
|
|
265
284
|
}
|
|
266
285
|
|
|
286
|
+
if (effortLevel) {
|
|
287
|
+
args.push("--effort", effortLevel);
|
|
288
|
+
}
|
|
289
|
+
|
|
267
290
|
return args;
|
|
268
291
|
}
|
|
269
292
|
|
package/server/agent/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { refreshCredentials } from "../system/credentials.js";
|
|
|
6
6
|
import { loadMcpConfig, loadSettings } from "../system/config.js";
|
|
7
7
|
import type { Role } from "../../src/config/roles.js";
|
|
8
8
|
import { buildSystemPrompt } from "./prompt.js";
|
|
9
|
+
import { loadMemorySnapshot } from "../workspace/memory/snapshot.js";
|
|
9
10
|
import { CONTAINER_WORKSPACE_PATH, buildMcpConfig, getActivePlugins, prepareUserServers, resolveMcpConfigPaths, userServerAllowedToolNames } from "./config.js";
|
|
10
11
|
import { validateStdioPackages } from "./mcpHealth.js";
|
|
11
12
|
import type { Attachment } from "@mulmobridge/protocol";
|
|
@@ -65,11 +66,15 @@ export async function* runAgent({
|
|
|
65
66
|
await refreshCredentials();
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// Pre-load memory once (atomic vs topic format chosen inside
|
|
70
|
+
// `loadMemorySnapshot`) so prompt assembly itself stays sync.
|
|
71
|
+
const memorySnapshot = await loadMemorySnapshot(workspacePath);
|
|
68
72
|
const fullSystemPrompt = buildSystemPrompt({
|
|
69
73
|
role,
|
|
70
74
|
workspacePath: useDocker ? CONTAINER_WORKSPACE_PATH : workspacePath,
|
|
71
75
|
useDocker,
|
|
72
76
|
userTimezone,
|
|
77
|
+
memorySnapshot,
|
|
73
78
|
});
|
|
74
79
|
|
|
75
80
|
// --debug: dump the full system prompt on the first message of each session.
|
|
@@ -134,6 +139,7 @@ export async function* runAgent({
|
|
|
134
139
|
activePlugins,
|
|
135
140
|
mcpConfigPath: hasMcp ? mcpPaths.argPath : undefined,
|
|
136
141
|
extraAllowedTools: [...settings.extraAllowedTools, ...userServerAllowedTools],
|
|
142
|
+
effortLevel: settings.effortLevel,
|
|
137
143
|
abortSignal,
|
|
138
144
|
userTimezone,
|
|
139
145
|
useDocker,
|
package/server/agent/prompt.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import { loadAllMemoryEntriesSync } from "../workspace/memory/io.js";
|
|
4
3
|
import type { MemoryEntry } from "../workspace/memory/types.js";
|
|
5
|
-
import { hasTopicFormat } from "../workspace/memory/topic-detect.js";
|
|
6
|
-
import { loadAllTopicFilesSync } from "../workspace/memory/topic-io.js";
|
|
7
4
|
import type { TopicMemoryFile } from "../workspace/memory/topic-types.js";
|
|
5
|
+
import type { MemorySnapshot } from "../workspace/memory/snapshot.js";
|
|
8
6
|
import type { Role } from "../../src/config/roles.js";
|
|
9
7
|
import { getActiveToolDescriptors, MCP_SERVER_ID } from "./activeTools.js";
|
|
10
8
|
import { WORKSPACE_DIRS, WORKSPACE_FILES } from "../workspace/paths.js";
|
|
@@ -68,6 +66,18 @@ Treat the markers as the source of truth for **which** files the user means when
|
|
|
68
66
|
|
|
69
67
|
When the user wants to transform existing images, call \`editImages\` with \`imagePaths\` set to an array of one or more workspace paths (single image: a one-element array). Pull the paths from the \`[Attached file: …]\` markers, from earlier tool results in this conversation, or from explicit paths the user mentions in plain text. When several markers are present and the request reads as a multi-image instruction ("combine these", "merge", "use both", etc.), include every relevant path in the array, in the order they appeared. \`editImages\` is fully stateless — it has no concept of a "currently selected" image, so the array is the only signal of which images to edit.
|
|
70
68
|
|
|
69
|
+
## Referring to files in chat replies
|
|
70
|
+
|
|
71
|
+
When you finish creating, updating, or surfacing a file in your reply (PDF, Markdown, HTML, image, spreadsheet, chart, etc.), present it to the user as a **Markdown link**:
|
|
72
|
+
|
|
73
|
+
\`[<short label or filename>](<workspace-relative-path>)\`
|
|
74
|
+
|
|
75
|
+
- ALWAYS use the Markdown link form so the UI renders it as a clickable link. Example: \`[summary.pdf](artifacts/documents/2026/05/summary.pdf)\`, or \`[updated wiki](data/wiki/pages/notes.md)\`.
|
|
76
|
+
- NEVER write the path as inline code (e.g. \`\\\`artifacts/foo.pdf\\\`\`) — that renders as non-clickable code and forces the user to copy / paste.
|
|
77
|
+
- NEVER write the path as plain text (e.g. "Open artifacts/foo.pdf to review") — same problem.
|
|
78
|
+
- The link path is the same **workspace-relative** form used everywhere else: no leading slash, no \`file://\`, no \`/api/files/...\` URL. The host resolves it to the right surface (Files panel preview / wiki page / canvas) when the user clicks.
|
|
79
|
+
- A short follow-up sentence like "Open it to review" or "ご確認ください" is fine, but the path itself MUST be inside the \`[...](...)\` wrapper.
|
|
80
|
+
|
|
71
81
|
## Task Scheduling
|
|
72
82
|
|
|
73
83
|
Skills and tasks can be scheduled via SKILL.md frontmatter (\`schedule: "daily HH:MM"\` or \`schedule: "interval Nh"\`). When the user asks to schedule something, recommend an appropriate frequency:
|
|
@@ -154,24 +164,24 @@ export function prependJournalPointer(message: string, workspacePath: string): s
|
|
|
154
164
|
// `readTypedMemoryEntries` / `readLegacyMemoryFile` /
|
|
155
165
|
// `formatMemoryEntryForPrompt` go with them. See
|
|
156
166
|
// `server/index.ts` for the full cleanup sweep.
|
|
157
|
-
export function buildMemoryContext(workspacePath: string): string {
|
|
167
|
+
export function buildMemoryContext(snapshot: MemorySnapshot, workspacePath: string): string {
|
|
158
168
|
const parts: string[] = [];
|
|
159
169
|
|
|
160
|
-
if (
|
|
170
|
+
if (snapshot.format === "topic") {
|
|
161
171
|
// Post-swap (topic format active): each topic file lands in the
|
|
162
172
|
// prompt as a single block — header + section index + body.
|
|
163
173
|
// The atomic / legacy readers are intentionally skipped here:
|
|
164
174
|
// once the topic layout is in place the user has acknowledged
|
|
165
175
|
// the cluster and the atomic entries have been parked under
|
|
166
176
|
// `.atomic-backup/`.
|
|
167
|
-
const topic =
|
|
177
|
+
const topic = formatTopicFiles(snapshot.files);
|
|
168
178
|
if (topic) parts.push(topic);
|
|
169
179
|
} else {
|
|
170
180
|
// Pre-swap: union of typed atomic entries (#1029) and the
|
|
171
181
|
// legacy `memory.md` (#1029 PR-A). Same dual-mode behaviour
|
|
172
182
|
// PR-B of #1029 shipped — preserved unchanged here so users
|
|
173
183
|
// without topic format keep seeing their memory.
|
|
174
|
-
const atomic =
|
|
184
|
+
const atomic = formatTypedMemoryEntries(snapshot.entries);
|
|
175
185
|
if (atomic) parts.push(atomic);
|
|
176
186
|
const legacy = readLegacyMemoryFile(workspacePath);
|
|
177
187
|
if (legacy) parts.push(legacy);
|
|
@@ -283,15 +293,20 @@ Keep entries short — name + description + a few lines of body at most. Bias to
|
|
|
283
293
|
// the workspace uses the topic layout (post-#1070 swap), emits the
|
|
284
294
|
// topic-format rules (find-or-create `<type>/<topic>.md`, append
|
|
285
295
|
// bullets under H2). Otherwise emits the atomic-format rules from
|
|
286
|
-
// #1029 PR-B (one fact per `<type>_<slug>.md`).
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
296
|
+
// #1029 PR-B (one fact per `<type>_<slug>.md`). Both this section
|
|
297
|
+
// and `buildMemoryContext` derive format from the same `snapshot`
|
|
298
|
+
// so write rules and read context stay consistent — including in
|
|
299
|
+
// Docker runs where `workspacePath="/workspace"` doesn't match the
|
|
300
|
+
// host path the snapshot was loaded from (Codex review on #1280).
|
|
301
|
+
export function buildMemoryManagementSection(snapshot: MemorySnapshot): string {
|
|
302
|
+
return snapshot.format === "topic" ? TOPIC_MEMORY_MANAGEMENT : ATOMIC_MEMORY_MANAGEMENT;
|
|
291
303
|
}
|
|
292
304
|
|
|
293
|
-
|
|
294
|
-
|
|
305
|
+
// Pure formatters — I/O happens once via `loadMemorySnapshot` before
|
|
306
|
+
// `buildSystemPrompt` is called (see `server/agent/index.ts`). Keeps
|
|
307
|
+
// prompt assembly side-effect-free per section.
|
|
308
|
+
|
|
309
|
+
function formatTopicFiles(files: readonly TopicMemoryFile[]): string | null {
|
|
295
310
|
if (files.length === 0) return null;
|
|
296
311
|
return files.map(formatTopicFileForPrompt).join("\n\n---\n\n");
|
|
297
312
|
}
|
|
@@ -303,13 +318,7 @@ function formatTopicFileForPrompt(file: TopicMemoryFile): string {
|
|
|
303
318
|
return body ? `${tagLine}\n${body}` : tagLine;
|
|
304
319
|
}
|
|
305
320
|
|
|
306
|
-
function
|
|
307
|
-
// Use the validated loader rather than reading raw files directly:
|
|
308
|
-
// a corrupt frontmatter (mid-edit, malformed YAML) is logged and
|
|
309
|
-
// skipped by `loadAllMemoryEntriesSync` instead of leaking into the
|
|
310
|
-
// system prompt. This also keeps the skip rules (MEMORY.md /
|
|
311
|
-
// dotfiles / non-files) defined in exactly one place.
|
|
312
|
-
const entries = loadAllMemoryEntriesSync(workspacePath);
|
|
321
|
+
function formatTypedMemoryEntries(entries: readonly MemoryEntry[]): string | null {
|
|
313
322
|
if (entries.length === 0) return null;
|
|
314
323
|
return entries.map(formatMemoryEntryForPrompt).join("\n\n");
|
|
315
324
|
}
|
|
@@ -522,6 +531,10 @@ export interface SystemPromptParams {
|
|
|
522
531
|
* user every turn. Missing or invalid values fall back to
|
|
523
532
|
* server-local date only. */
|
|
524
533
|
userTimezone?: string;
|
|
534
|
+
/** Pre-loaded memory snapshot — caller awaits `loadMemorySnapshot`
|
|
535
|
+
* before invoking `buildSystemPrompt` so prompt assembly stays
|
|
536
|
+
* synchronous and side-effect-free for the memory section. */
|
|
537
|
+
memorySnapshot: MemorySnapshot;
|
|
525
538
|
}
|
|
526
539
|
|
|
527
540
|
// Accept IANA-looking strings only. Anything else (including
|
|
@@ -681,15 +694,15 @@ interface NamedSection {
|
|
|
681
694
|
const SYSTEM_PROMPT_WARN_THRESHOLD_CHARS = 20000;
|
|
682
695
|
|
|
683
696
|
export function buildSystemPrompt(params: SystemPromptParams): string {
|
|
684
|
-
const { role, workspacePath, useDocker, userTimezone } = params;
|
|
697
|
+
const { role, workspacePath, useDocker, userTimezone, memorySnapshot } = params;
|
|
685
698
|
|
|
686
699
|
const sections: NamedSection[] = [
|
|
687
700
|
{ name: "base", content: SYSTEM_PROMPT },
|
|
688
701
|
{ name: "role", content: role.prompt },
|
|
689
702
|
{ name: "workspace", content: `Workspace directory: ${workspacePath}` },
|
|
690
703
|
{ name: "time", content: buildTimeSection(new Date(), userTimezone) },
|
|
691
|
-
{ name: "memory", content: buildMemoryContext(workspacePath) },
|
|
692
|
-
{ name: "memory-management", content: buildMemoryManagementSection(
|
|
704
|
+
{ name: "memory", content: buildMemoryContext(memorySnapshot, workspacePath) },
|
|
705
|
+
{ name: "memory-management", content: buildMemoryManagementSection(memorySnapshot) },
|
|
693
706
|
{ name: "sandbox", content: useDocker ? SANDBOX_TOOLS_HINT : null },
|
|
694
707
|
{ name: "wiki", content: buildWikiContext(workspacePath) },
|
|
695
708
|
{ name: "sources", content: buildSourcesContext(workspacePath) },
|
|
@@ -36,6 +36,7 @@ import { ACCOUNTING_ACTIONS } from "../../../src/plugins/accounting/actions.js";
|
|
|
36
36
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
37
37
|
import { bindRoute } from "../../utils/router.js";
|
|
38
38
|
import { log } from "../../system/logger/index.js";
|
|
39
|
+
import { asyncHandler } from "../../utils/asyncHandler.js";
|
|
39
40
|
|
|
40
41
|
const router = Router();
|
|
41
42
|
|
|
@@ -335,32 +336,38 @@ async function dispatch(body: AccountingActionBody): Promise<unknown> {
|
|
|
335
336
|
bindRoute(
|
|
336
337
|
router,
|
|
337
338
|
API_ROUTES.accounting.dispatch,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
log.info("accounting", "POST dispatch: start", { action });
|
|
350
|
-
try {
|
|
351
|
-
const result = await dispatch(body);
|
|
352
|
-
log.info("accounting", "POST dispatch: ok", { action });
|
|
353
|
-
res.json(result);
|
|
354
|
-
} catch (err) {
|
|
355
|
-
if (err instanceof AccountingError) {
|
|
356
|
-
log.warn("accounting", "POST dispatch: error", { action, status: err.status, message: err.message });
|
|
357
|
-
res.status(err.status).json({ error: err.message, details: err.details });
|
|
339
|
+
asyncHandler<Request<object, unknown, AccountingActionBody>, Response<unknown | AccountingErrorResponse>>(
|
|
340
|
+
"accounting",
|
|
341
|
+
"accounting dispatch failed",
|
|
342
|
+
async (req, res) => {
|
|
343
|
+
// Validate the body shape up front so a missing / non-object body
|
|
344
|
+
// surfaces as a 400 instead of crashing `dispatch` and bubbling
|
|
345
|
+
// through to the 500 catch-all.
|
|
346
|
+
const { body } = req;
|
|
347
|
+
if (!body || typeof body !== "object" || typeof body.action !== "string") {
|
|
348
|
+
log.warn("accounting", "POST dispatch: invalid body");
|
|
349
|
+
res.status(400).json({ error: "request body must be an object with a string `action` field" });
|
|
358
350
|
return;
|
|
359
351
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
352
|
+
const { action } = body;
|
|
353
|
+
log.info("accounting", "POST dispatch: start", { action });
|
|
354
|
+
try {
|
|
355
|
+
const result = await dispatch(body);
|
|
356
|
+
log.info("accounting", "POST dispatch: ok", { action });
|
|
357
|
+
res.json(result);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
// Domain errors (AccountingError) map to 4xx with `details`.
|
|
360
|
+
// Anything else rethrows — the asyncHandler wrapper catches
|
|
361
|
+
// it, logs `unexpected error`, and returns a generic 500.
|
|
362
|
+
if (err instanceof AccountingError) {
|
|
363
|
+
log.warn("accounting", "POST dispatch: error", { action, status: err.status, message: err.message });
|
|
364
|
+
res.status(err.status).json({ error: err.message, details: err.details });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
),
|
|
364
371
|
);
|
|
365
372
|
|
|
366
373
|
export default router;
|
|
@@ -27,6 +27,7 @@ import { maybeIndexSession } from "../../workspace/chat-index/index.js";
|
|
|
27
27
|
import { maybeAppendWikiBacklinks } from "../../workspace/wiki-backlinks/index.js";
|
|
28
28
|
import { log } from "../../system/logger/index.js";
|
|
29
29
|
import { logBackgroundError } from "../../utils/logBackgroundError.js";
|
|
30
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
30
31
|
import { createArgsCache, recordToolEvent } from "../../workspace/tool-trace/index.js";
|
|
31
32
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
32
33
|
import { EVENT_TYPES } from "../../../src/types/events.js";
|
|
@@ -46,7 +47,6 @@ import { env } from "../../system/env.js";
|
|
|
46
47
|
import type { Attachment } from "@mulmobridge/protocol";
|
|
47
48
|
import { isImagePath, loadImageBase64 } from "../../utils/files/image-store.js";
|
|
48
49
|
import { isAttachmentPath, loadAttachmentBase64, inferMimeFromExtension, saveAttachment } from "../../utils/files/attachment-store.js";
|
|
49
|
-
import { errorMessage } from "../../utils/errors.js";
|
|
50
50
|
|
|
51
51
|
const router = Router();
|
|
52
52
|
const PORT = env.port;
|
|
@@ -810,7 +810,7 @@ async function resolveSkillMetadata(skillName: string): Promise<SkillMetadata> {
|
|
|
810
810
|
// can still collapse it; just leave metadata empty.
|
|
811
811
|
log.warn("agent", "skill metadata lookup failed — emitting entry without scope/path/description/body", {
|
|
812
812
|
skillName,
|
|
813
|
-
error:
|
|
813
|
+
error: errorMessage(err),
|
|
814
814
|
});
|
|
815
815
|
return { scope: "unknown", path: null, description: null, body: null };
|
|
816
816
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// POST /api/config/refresh — wraps `refreshScheduledSkills()` +
|
|
2
|
+
// `refreshUserTasks()` into one endpoint so the config-refresh
|
|
3
|
+
// PostToolUse hook (#1283) can fire-and-forget after Write/Edit of
|
|
4
|
+
// the relevant config files without knowing which refreshers exist.
|
|
5
|
+
// Serves the `mc-manage-skills` + `mc-manage-automations` preset
|
|
6
|
+
// skills (split out from the original `mc-settings` in #1295).
|
|
7
|
+
//
|
|
8
|
+
// Best-effort by design: failures from one refresher don't block the
|
|
9
|
+
// other; the response is always 200 with a per-refresher status so the
|
|
10
|
+
// caller (the hook) can log on errors but never has to abort.
|
|
11
|
+
|
|
12
|
+
import { Router, Request, Response } from "express";
|
|
13
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
14
|
+
import { log } from "../../system/logger/index.js";
|
|
15
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
16
|
+
import { refreshScheduledSkills } from "../../workspace/skills/scheduler.js";
|
|
17
|
+
import { refreshUserTasks } from "../../workspace/skills/user-tasks.js";
|
|
18
|
+
|
|
19
|
+
const router = Router();
|
|
20
|
+
|
|
21
|
+
interface RefreshOutcome {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
count?: number;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface RefreshResponse {
|
|
28
|
+
skills: RefreshOutcome;
|
|
29
|
+
userTasks: RefreshOutcome;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function safeRefresh(label: string, refresher: () => Promise<number>): Promise<RefreshOutcome> {
|
|
33
|
+
try {
|
|
34
|
+
const count = await refresher();
|
|
35
|
+
return { ok: true, count };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const error = errorMessage(err);
|
|
38
|
+
log.warn("config-refresh", `${label} refresh failed`, { error });
|
|
39
|
+
return { ok: false, error };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
router.post(API_ROUTES.config.refresh, async (_req: Request, res: Response<RefreshResponse>) => {
|
|
44
|
+
const [skills, userTasks] = await Promise.all([safeRefresh("skills", refreshScheduledSkills), safeRefresh("userTasks", refreshUserTasks)]);
|
|
45
|
+
log.debug("config-refresh", "refresh complete", { skills, userTasks });
|
|
46
|
+
res.json({ skills, userTasks });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export default router;
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
isAppSettingsPatch,
|
|
6
6
|
loadMcpConfig,
|
|
7
7
|
loadSettings,
|
|
8
|
+
normaliseAppSettingsPatch,
|
|
8
9
|
saveMcpConfig,
|
|
9
10
|
saveSettings,
|
|
10
11
|
toMcpEntries,
|
|
11
12
|
type AppSettings,
|
|
13
|
+
type AppSettingsPatch,
|
|
12
14
|
type McpConfigFile,
|
|
13
15
|
type McpServerEntry,
|
|
14
16
|
} from "../../system/config.js";
|
|
@@ -17,6 +19,7 @@ import { errorMessage } from "../../utils/errors.js";
|
|
|
17
19
|
import { isRecord } from "../../utils/types.js";
|
|
18
20
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
19
21
|
import { log } from "../../system/logger/index.js";
|
|
22
|
+
import { asyncHandler } from "../../utils/asyncHandler.js";
|
|
20
23
|
import { loadCustomDirs, saveCustomDirs, ensureCustomDirs, validateCustomDirs, type CustomDirEntry } from "../../workspace/custom-dirs.js";
|
|
21
24
|
import { loadReferenceDirs, saveReferenceDirs, validateReferenceDirs, type ReferenceDirEntry } from "../../workspace/reference-dirs.js";
|
|
22
25
|
|
|
@@ -67,16 +70,18 @@ function parseMcpPayloadOrFail(res: ConfigRes, servers: McpServerEntry[]): McpCo
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
// Run a filesystem save. On failure,
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
+
// Run a filesystem save. On failure, log the raw error server-side
|
|
74
|
+
// (full triage detail kept in logs), respond 500 with the safe
|
|
75
|
+
// `fallback` message, and return false so the caller can early-return.
|
|
76
|
+
// Returns true on success. The raw `err.message` deliberately doesn't
|
|
77
|
+
// reach the client — same threat model as `asyncHandler`.
|
|
73
78
|
function runSaveOrFail(res: ConfigRes, save: () => void, fallback: string): boolean {
|
|
74
79
|
try {
|
|
75
80
|
save();
|
|
76
81
|
return true;
|
|
77
82
|
} catch (err) {
|
|
78
83
|
log.error("config", `save failed: ${fallback}`, { error: errorMessage(err) });
|
|
79
|
-
serverError(res,
|
|
84
|
+
serverError(res, fallback);
|
|
80
85
|
return false;
|
|
81
86
|
}
|
|
82
87
|
}
|
|
@@ -134,7 +139,7 @@ router.put(API_ROUTES.config.base, (req: Request<unknown, unknown, PutConfigBody
|
|
|
134
139
|
res.json(buildFullResponse());
|
|
135
140
|
});
|
|
136
141
|
|
|
137
|
-
router.put(API_ROUTES.config.settings, (req: Request<unknown, unknown,
|
|
142
|
+
router.put(API_ROUTES.config.settings, (req: Request<unknown, unknown, AppSettingsPatch>, res: ConfigRes) => {
|
|
138
143
|
const { body } = req;
|
|
139
144
|
log.info("config", "PUT settings: start");
|
|
140
145
|
if (!isAppSettingsPatch(body)) {
|
|
@@ -147,8 +152,16 @@ router.put(API_ROUTES.config.settings, (req: Request<unknown, unknown, Partial<A
|
|
|
147
152
|
// that knows about only some fields (e.g. Tools tab sends only
|
|
148
153
|
// `extraAllowedTools`, Map tab sends only `googleMapsApiKey`)
|
|
149
154
|
// doesn't wipe fields owned by other tabs.
|
|
155
|
+
//
|
|
156
|
+
// `null` in the patch is a sentinel for "clear this field":
|
|
157
|
+
// normaliseAppSettingsPatch drops the entry from the patch, AND we
|
|
158
|
+
// must also delete it from the merged result so the existing
|
|
159
|
+
// value doesn't leak through the spread.
|
|
150
160
|
const existing = loadSettings();
|
|
151
|
-
const merged: AppSettings = { ...existing, ...body };
|
|
161
|
+
const merged: AppSettings = { ...existing, ...normaliseAppSettingsPatch(body) };
|
|
162
|
+
if (body.effortLevel === null) {
|
|
163
|
+
delete merged.effortLevel;
|
|
164
|
+
}
|
|
152
165
|
if (!runSaveOrFail(res, () => saveSettings(merged), "saveSettings failed")) {
|
|
153
166
|
return;
|
|
154
167
|
}
|
|
@@ -183,30 +196,29 @@ router.get(API_ROUTES.config.workspaceDirs, (_req: Request, res: Response<{ dirs
|
|
|
183
196
|
|
|
184
197
|
router.put(
|
|
185
198
|
API_ROUTES.config.workspaceDirs,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
asyncHandler<Request<unknown, unknown, { dirs: unknown }>, Response<{ dirs: CustomDirEntry[] } | ConfigErrorResponse>>(
|
|
200
|
+
"config",
|
|
201
|
+
"save failed",
|
|
202
|
+
async (req, res) => {
|
|
203
|
+
const { body } = req;
|
|
204
|
+
log.info("config", "PUT workspace-dirs: start");
|
|
205
|
+
if (!isRecord(body) || !("dirs" in body)) {
|
|
206
|
+
log.warn("config", "PUT workspace-dirs: invalid envelope");
|
|
207
|
+
badRequest(res, "expected { dirs: [...] }");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const result = validateCustomDirs(body.dirs);
|
|
211
|
+
if ("error" in result) {
|
|
212
|
+
log.warn("config", "PUT workspace-dirs: validation failed", { error: result.error });
|
|
213
|
+
badRequest(res, result.error);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
201
216
|
saveCustomDirs(result.entries);
|
|
202
217
|
ensureCustomDirs(result.entries);
|
|
203
218
|
log.info("config", "PUT workspace-dirs: ok", { dirs: result.entries.length });
|
|
204
219
|
res.json({ dirs: result.entries });
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
serverError(res, errorMessage(err, "save failed"));
|
|
208
|
-
}
|
|
209
|
-
},
|
|
220
|
+
},
|
|
221
|
+
),
|
|
210
222
|
);
|
|
211
223
|
|
|
212
224
|
// ── Reference directories (#455) ────────────────────────────────
|
|
@@ -217,29 +229,28 @@ router.get(API_ROUTES.config.referenceDirs, (_req: Request, res: Response<{ dirs
|
|
|
217
229
|
|
|
218
230
|
router.put(
|
|
219
231
|
API_ROUTES.config.referenceDirs,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
asyncHandler<Request<unknown, unknown, { dirs: unknown }>, Response<{ dirs: ReferenceDirEntry[] } | ConfigErrorResponse>>(
|
|
233
|
+
"config",
|
|
234
|
+
"save failed",
|
|
235
|
+
async (req, res) => {
|
|
236
|
+
const { body } = req;
|
|
237
|
+
log.info("config", "PUT reference-dirs: start");
|
|
238
|
+
if (!isRecord(body) || !("dirs" in body)) {
|
|
239
|
+
log.warn("config", "PUT reference-dirs: invalid envelope");
|
|
240
|
+
badRequest(res, "expected { dirs: [...] }");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const result = validateReferenceDirs(body.dirs);
|
|
244
|
+
if ("error" in result) {
|
|
245
|
+
log.warn("config", "PUT reference-dirs: validation failed", { error: result.error });
|
|
246
|
+
badRequest(res, result.error);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
235
249
|
saveReferenceDirs(result.entries);
|
|
236
250
|
log.info("config", "PUT reference-dirs: ok", { dirs: result.entries.length });
|
|
237
251
|
res.json({ dirs: result.entries });
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
serverError(res, errorMessage(err, "save failed"));
|
|
241
|
-
}
|
|
242
|
-
},
|
|
252
|
+
},
|
|
253
|
+
),
|
|
243
254
|
);
|
|
244
255
|
|
|
245
256
|
router.get(API_ROUTES.config.schedulerOverrides, (_req: Request, res: Response<{ overrides: ScheduleOverrides }>) => {
|
|
@@ -248,22 +259,24 @@ router.get(API_ROUTES.config.schedulerOverrides, (_req: Request, res: Response<{
|
|
|
248
259
|
|
|
249
260
|
router.put(
|
|
250
261
|
API_ROUTES.config.schedulerOverrides,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
asyncHandler<Request<unknown, unknown, { overrides: unknown }>, Response<{ overrides: ScheduleOverrides } | ConfigErrorResponse>>(
|
|
263
|
+
"config",
|
|
264
|
+
"save failed",
|
|
265
|
+
async (req, res) => {
|
|
266
|
+
const { body } = req;
|
|
267
|
+
log.info("config", "PUT scheduler-overrides: start");
|
|
268
|
+
if (!isRecord(body) || !("overrides" in body)) {
|
|
269
|
+
log.warn("config", "PUT scheduler-overrides: invalid envelope");
|
|
270
|
+
badRequest(res, "expected { overrides: { ... } }");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const raw = body.overrides;
|
|
274
|
+
if (!isRecord(raw)) {
|
|
275
|
+
log.warn("config", "PUT scheduler-overrides: overrides not an object");
|
|
276
|
+
badRequest(res, "overrides must be an object");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const overrides = raw as ScheduleOverrides;
|
|
267
280
|
saveSchedulerOverrides(overrides);
|
|
268
281
|
|
|
269
282
|
// Apply to running task-manager immediately
|
|
@@ -283,11 +296,8 @@ router.put(
|
|
|
283
296
|
|
|
284
297
|
log.info("config", "PUT scheduler-overrides: ok", { tasks: Object.keys(overrides).length });
|
|
285
298
|
res.json({ overrides: loadSchedulerOverrides() });
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
serverError(res, errorMessage(err, "save failed"));
|
|
289
|
-
}
|
|
290
|
-
},
|
|
299
|
+
},
|
|
300
|
+
),
|
|
291
301
|
);
|
|
292
302
|
|
|
293
303
|
export default router;
|