mulmoclaude 0.6.0 → 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-BbgSjFQ8.js → index-BwrlMMHr.js} +178 -141
- 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 +9 -7
- package/server/accounting/eventPublisher.ts +2 -1
- package/server/accounting/snapshotCache.ts +2 -1
- package/server/agent/activeTools.ts +16 -6
- 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/mcp-server.ts +9 -6
- package/server/agent/mcp-tools/index.ts +15 -2
- package/server/agent/mcp-tools/notify.ts +20 -2
- 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 +86 -68
- 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/photo-locations.ts +79 -0
- package/server/api/routes/plugins.ts +11 -0
- 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 +130 -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 +138 -16
- package/server/utils/asyncHandler.ts +75 -0
- package/server/utils/exif.ts +321 -0
- package/server/utils/files/accounting-io.ts +19 -20
- package/server/utils/files/attachment-store.ts +69 -12
- 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 +16 -0
- package/server/workspace/photo-locations/index.ts +149 -0
- package/server/workspace/photo-locations/list.ts +124 -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 +78 -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/NewsView.vue +2 -1
- package/src/components/RolesView.vue +14 -5
- package/src/components/SettingsMapTab.vue +140 -0
- package/src/components/SettingsMcpTab.vue +15 -10
- package/src/components/SettingsModal.vue +138 -112
- package/src/components/SettingsModelTab.vue +121 -0
- package/src/components/SettingsPhotosTab.vue +118 -0
- package/src/components/SourcesManager.vue +4 -3
- package/src/components/StackView.vue +43 -12
- package/src/composables/useContentDisplay.ts +16 -0
- package/src/composables/useFileDropZone.ts +148 -0
- package/src/composables/useImageErrorRepair.ts +29 -19
- package/src/composables/useSkillsList.ts +2 -1
- package/src/config/apiRoutes.ts +24 -0
- package/src/config/roles.ts +121 -70
- package/src/config/systemFileDescriptors.ts +2 -2
- package/src/config/toolNames.ts +26 -0
- package/src/index.css +26 -0
- package/src/lang/de.ts +70 -1
- package/src/lang/en.ts +69 -1
- package/src/lang/es.ts +69 -1
- package/src/lang/fr.ts +69 -1
- package/src/lang/ja.ts +69 -1
- package/src/lang/ko.ts +68 -1
- package/src/lang/pt-BR.ts +69 -1
- package/src/lang/zh.ts +67 -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 +8 -0
- package/src/plugins/_extras.ts +6 -2
- package/src/plugins/_generated/metas.ts +4 -0
- package/src/plugins/_generated/registrations.ts +4 -0
- package/src/plugins/_generated/server-bindings.ts +6 -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/manageSkills/View.vue +12 -4
- package/src/plugins/markdown/View.vue +6 -0
- package/src/plugins/photoLocations/View.vue +231 -0
- package/src/plugins/photoLocations/definition.ts +47 -0
- package/src/plugins/photoLocations/index.ts +38 -0
- package/src/plugins/photoLocations/meta.ts +35 -0
- package/src/plugins/presentMulmoScript/View.vue +76 -7
- package/src/plugins/presentMulmoScript/helpers.ts +15 -0
- 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 +15 -16
- package/src/plugins/spreadsheet/View.vue +4 -0
- 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/dom/iframeHeightClamp.ts +42 -0
- package/src/utils/format/bytes.ts +41 -0
- package/src/utils/format/date.ts +14 -2
- package/src/utils/image/imageRepairInlineScript.ts +192 -41
- package/src/utils/markdown/sanitize.ts +68 -0
- package/src/utils/markdown/setup.ts +36 -0
- package/src/utils/markdown/wikiEmbedHandlers.ts +170 -0
- package/src/utils/markdown/wikiEmbeds.ts +141 -0
- package/src/utils/markdown/workspaceLinkify.ts +73 -0
- package/src/utils/path/workspaceLinkRouter.ts +17 -1
- package/client/assets/index-ECD0lgIv.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
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
// `getActiveToolDescriptors(role)` produces a single list of
|
|
20
20
|
// `ActiveToolDescriptor` rows and the three call sites read whichever
|
|
21
21
|
// fields they need (name only / name + prompt / name + endpoint).
|
|
22
|
-
//
|
|
23
|
-
//
|
|
22
|
+
// EVERY tool source (static-gui, static-mcp, runtime) is gated by
|
|
23
|
+
// `role.availablePlugins` — runtime plugins used to be auto-included
|
|
24
|
+
// regardless, which surfaced as a real bug (preset plugins like
|
|
25
|
+
// `manageRecipes` leaked into every role even though `cookingCoach`
|
|
26
|
+
// was their intended home). The MCP-prefixed full
|
|
24
27
|
// name is precomputed once so callers don't have to re-derive it.
|
|
25
28
|
|
|
26
29
|
import type { Role } from "../../src/config/roles.js";
|
|
@@ -109,10 +112,17 @@ export function getActiveToolDescriptors(role: Role): ActiveToolDescriptor[] {
|
|
|
109
112
|
for (const plugin of getRuntimePlugins()) {
|
|
110
113
|
const def = plugin.definition;
|
|
111
114
|
if (seen.has(def.name)) continue; // runtime-registry collision
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
115
|
+
// Runtime plugins (preset + user-installed alike) are now gated
|
|
116
|
+
// by `role.availablePlugins`, mirroring the static-GUI / static-MCP
|
|
117
|
+
// loops above. Previously they were auto-included regardless of
|
|
118
|
+
// role — that broke the role's stated promise of "exactly these
|
|
119
|
+
// tools" and surfaced as a real bug when a non-cooking role
|
|
120
|
+
// started seeing `manageRecipes`. Roles that want a runtime
|
|
121
|
+
// plugin must list its `toolName` in `availablePlugins`. The
|
|
122
|
+
// Settings → Roles UI lets users add tool names per role; preset
|
|
123
|
+
// plugin names land in the `general` role's `availablePlugins`
|
|
124
|
+
// out of the box.
|
|
125
|
+
if (!allowed.has(def.name)) continue;
|
|
116
126
|
out.push({
|
|
117
127
|
name: def.name,
|
|
118
128
|
fullName: fullNameFor(def.name),
|
|
@@ -143,6 +143,7 @@ async function* runClaudeAgent(input: AgentInput): AsyncGenerator<AgentEvent> {
|
|
|
143
143
|
claudeSessionId: input.sessionToken,
|
|
144
144
|
mcpConfigPath: input.mcpConfigPath,
|
|
145
145
|
extraAllowedTools: input.extraAllowedTools,
|
|
146
|
+
effortLevel: input.effortLevel,
|
|
146
147
|
});
|
|
147
148
|
|
|
148
149
|
const proc = spawnClaude(input.useDocker, input.workspacePath, cliArgs, input.sessionId);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { Attachment } from "@mulmobridge/protocol";
|
|
11
11
|
import type { Role } from "../../../src/config/roles.js";
|
|
12
|
+
import type { EffortLevel } from "../../system/config.js";
|
|
12
13
|
import type { AgentEvent } from "../stream.js";
|
|
13
14
|
|
|
14
15
|
/** Inputs the orchestrator passes to a backend for one user turn.
|
|
@@ -37,6 +38,8 @@ export interface AgentInput {
|
|
|
37
38
|
mcpConfigPath?: string;
|
|
38
39
|
/** Extra allowed-tool names from settings + user MCP servers. */
|
|
39
40
|
extraAllowedTools: string[];
|
|
41
|
+
/** Reasoning effort from settings (#1323). Undefined → flag omitted. */
|
|
42
|
+
effortLevel?: EffortLevel;
|
|
40
43
|
/** When fired, the backend must terminate any in-flight
|
|
41
44
|
* subprocess / connection. */
|
|
42
45
|
abortSignal?: AbortSignal;
|
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,
|
|
@@ -162,12 +162,15 @@ const runtimeReady: Promise<void> = (async () => {
|
|
|
162
162
|
const endpoint = API_ROUTES.plugins.runtimeDispatch.replace(":pkg", encodeURIComponent(plugin.name));
|
|
163
163
|
ALL_TOOLS[plugin.definition.name] = fromPackage(plugin.definition, endpoint);
|
|
164
164
|
}
|
|
165
|
-
// Runtime plugins are
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
// Runtime plugins are gated by `role.availablePlugins` (mirrored
|
|
166
|
+
// here through the PLUGIN_NAMES env set by the parent's
|
|
167
|
+
// `getActivePlugins(role)`). Previously every runtime plugin was
|
|
168
|
+
// auto-active in every role, which leaked preset plugins like
|
|
169
|
+
// `manageRecipes` into roles that shouldn't expose them. The
|
|
170
|
+
// intersection is now: ALL_TOOLS includes both static + runtime
|
|
171
|
+
// entries, but only the names PLUGIN_NAMES authorises become live
|
|
172
|
+
// tools.
|
|
173
|
+
activeNames = [...PLUGIN_NAMES];
|
|
171
174
|
tools = activeNames.map((name) => ALL_TOOLS[name]).filter(Boolean);
|
|
172
175
|
} catch (err) {
|
|
173
176
|
process.stderr.write(`[mcp-server] runtime plugin load failed; static tools only: ${String(err)}\n`);
|
|
@@ -5,6 +5,17 @@ import { errorMessage } from "../../utils/errors.js";
|
|
|
5
5
|
import { notFound, sendError, serverError } from "../../utils/httpError.js";
|
|
6
6
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
7
7
|
|
|
8
|
+
// Per-call context the MCP bridge threads through to the tool handler.
|
|
9
|
+
// Currently just the chat session id (extracted from the `?session=`
|
|
10
|
+
// query string the bridge always appends, see mcp-server.ts), so a
|
|
11
|
+
// tool like `notify` can mark its outgoing notification with a
|
|
12
|
+
// click-target back to the originating chat. Optional because the
|
|
13
|
+
// HTTP route is also reachable by non-bridge callers (tests, ad-hoc
|
|
14
|
+
// scripts) that have no session.
|
|
15
|
+
export interface McpToolContext {
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
export interface McpTool {
|
|
9
20
|
definition: {
|
|
10
21
|
name: string;
|
|
@@ -13,7 +24,7 @@ export interface McpTool {
|
|
|
13
24
|
};
|
|
14
25
|
requiredEnv?: string[];
|
|
15
26
|
prompt?: string;
|
|
16
|
-
handler: (args: Record<string, unknown
|
|
27
|
+
handler: (args: Record<string, unknown>, ctx?: McpToolContext) => Promise<string>;
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
export const mcpTools: McpTool[] = [readXPost, searchX, notify];
|
|
@@ -52,7 +63,9 @@ mcpToolsRouter.post(API_ROUTES.mcpTools.invoke, async (req: Request<McpToolParam
|
|
|
52
63
|
return;
|
|
53
64
|
}
|
|
54
65
|
try {
|
|
55
|
-
const
|
|
66
|
+
const sessionRaw = typeof req.query.session === "string" ? req.query.session : "";
|
|
67
|
+
const ctx: McpToolContext | undefined = sessionRaw.length > 0 ? { sessionId: sessionRaw } : undefined;
|
|
68
|
+
const result = await tool.handler(req.body, ctx);
|
|
56
69
|
res.json({ result });
|
|
57
70
|
} catch (err) {
|
|
58
71
|
serverError(res, errorMessage(err));
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// bell side effects.
|
|
3
3
|
|
|
4
4
|
import { publishNotification } from "../../events/notifications.js";
|
|
5
|
-
import { NOTIFICATION_KINDS } from "../../../src/types/notification.js";
|
|
5
|
+
import { NOTIFICATION_ACTION_TYPES, NOTIFICATION_KINDS, NOTIFICATION_VIEWS } from "../../../src/types/notification.js";
|
|
6
|
+
import type { McpToolContext } from "./index.js";
|
|
6
7
|
|
|
7
8
|
export type NotifyPublishFn = typeof publishNotification;
|
|
8
9
|
|
|
@@ -10,6 +11,21 @@ export interface NotifyToolDeps {
|
|
|
10
11
|
publish: NotifyPublishFn;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
// When the bridge threads a chat session through, mark the
|
|
15
|
+
// notification's primary action as "open the originating chat" so
|
|
16
|
+
// the user can click the bell entry and land back on the session
|
|
17
|
+
// that produced it (typically a scheduled / background chat that
|
|
18
|
+
// finished while they were elsewhere). Without a session id, fall
|
|
19
|
+
// back to plain push — entry is just dismissed on click, which is
|
|
20
|
+
// the unchanged pre-fix behaviour.
|
|
21
|
+
function buildNavigateAction(ctx?: McpToolContext) {
|
|
22
|
+
if (!ctx?.sessionId || ctx.sessionId.length === 0) return undefined;
|
|
23
|
+
return {
|
|
24
|
+
type: NOTIFICATION_ACTION_TYPES.navigate,
|
|
25
|
+
target: { view: NOTIFICATION_VIEWS.chat, sessionId: ctx.sessionId },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
export function makeNotifyTool(deps: NotifyToolDeps) {
|
|
14
30
|
return {
|
|
15
31
|
definition: {
|
|
@@ -37,16 +53,18 @@ export function makeNotifyTool(deps: NotifyToolDeps) {
|
|
|
37
53
|
"This is the canonical built-in notification path: it fans out to the web bell, any active bridge transport, and macOS Reminders (when MACOS_REMINDER_NOTIFICATIONS=1 + darwin), and has NO active-user suppression — if the user asks for a notification, fire one. " +
|
|
38
54
|
"After firing, briefly tell the user you sent the notification.",
|
|
39
55
|
|
|
40
|
-
async handler(args: Record<string, unknown
|
|
56
|
+
async handler(args: Record<string, unknown>, ctx?: McpToolContext): Promise<string> {
|
|
41
57
|
const title = typeof args.title === "string" ? args.title.trim() : "";
|
|
42
58
|
if (!title) return "notify: `title` is required (non-empty string).";
|
|
43
59
|
const bodyRaw = typeof args.body === "string" ? args.body.trim() : "";
|
|
44
60
|
const body = bodyRaw.length > 0 ? bodyRaw : undefined;
|
|
45
61
|
|
|
62
|
+
const action = buildNavigateAction(ctx);
|
|
46
63
|
deps.publish({
|
|
47
64
|
kind: NOTIFICATION_KINDS.push,
|
|
48
65
|
title,
|
|
49
66
|
body,
|
|
67
|
+
...(action ? { action } : {}),
|
|
50
68
|
});
|
|
51
69
|
return body ? `Notification sent: ${title}\n${body}` : `Notification sent: ${title}`;
|
|
52
70
|
},
|
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;
|