mulmoclaude 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mulmoclaude.js +7 -24
- package/client/assets/html2canvas-Cx501zZr-DiKaqnKs.js +5 -0
- package/client/assets/{index-eHWB79u5.js → index-C94GcmNa.js} +203 -198
- package/client/assets/index-CY-WpQUm.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-5ipqh8Pe.js} +5 -5
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/client/index.html +2 -4
- package/package.json +17 -15
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/backend/claude-code.ts +170 -0
- package/server/agent/backend/index.ts +14 -0
- package/server/agent/backend/types.ts +65 -0
- package/server/agent/index.ts +31 -159
- package/server/agent/mcp-server.ts +88 -10
- package/server/agent/mcp-tools/index.ts +8 -7
- package/server/agent/mcp-tools/notify.ts +76 -0
- package/server/agent/mcp-tools/x.ts +12 -2
- package/server/agent/plugin-names.ts +10 -4
- package/server/agent/prompt.ts +187 -26
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +3 -3
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +99 -4
- package/server/api/routes/chart.ts +13 -0
- package/server/api/routes/chat-index.ts +2 -1
- package/server/api/routes/config.ts +35 -8
- package/server/api/routes/files.ts +75 -24
- package/server/api/routes/html.ts +15 -2
- package/server/api/routes/image.ts +75 -20
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/news.ts +146 -0
- package/server/api/routes/notifications.ts +58 -2
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +73 -91
- package/server/api/routes/presentHtml.ts +9 -0
- package/server/api/routes/roles.ts +12 -2
- package/server/api/routes/scheduler.ts +20 -11
- package/server/api/routes/schedulerTasks.ts +58 -21
- package/server/api/routes/sessions.ts +15 -4
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +26 -5
- package/server/api/routes/sources.ts +8 -7
- package/server/api/routes/todos.ts +30 -0
- package/server/api/routes/todosColumnsHandlers.ts +13 -27
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki/frontmatter.ts +86 -0
- package/server/api/routes/wiki.ts +335 -75
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +32 -8
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +26 -16
- package/server/events/resolveRelayBridgeOptions.ts +125 -0
- package/server/index.ts +72 -49
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +15 -5
- package/server/system/macosNotify.ts +152 -0
- package/server/utils/errors.ts +11 -2
- package/server/utils/fetch.ts +54 -0
- package/server/utils/files/atomic.ts +18 -17
- package/server/utils/files/image-store.ts +19 -13
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-image-fill.ts +131 -0
- package/server/utils/files/markdown-store.ts +22 -6
- package/server/utils/files/naming.ts +20 -10
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +4 -4
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/spreadsheet-store.ts +15 -10
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +30 -4
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/id.ts +40 -8
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/logPreview.ts +24 -0
- package/server/utils/markdown.ts +5 -5
- package/server/utils/port.d.mts +6 -0
- package/server/utils/port.mjs +48 -0
- package/server/utils/promptMeta.ts +32 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/slug.ts +65 -4
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/index.ts +1 -1
- package/server/workspace/chat-index/summarizer.ts +5 -5
- package/server/workspace/custom-dirs.ts +5 -5
- package/server/workspace/helps/gemini.md +57 -0
- package/server/workspace/helps/index.md +2 -1
- package/server/workspace/helps/sources.md +42 -0
- package/server/workspace/helps/wiki.md +40 -5
- package/server/workspace/journal/archivist-cli.ts +121 -0
- package/server/workspace/journal/{archivist.ts → archivist-schemas.ts} +12 -120
- package/server/workspace/journal/dailyPass.ts +78 -38
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +56 -5
- package/server/workspace/journal/memoryExtractor.ts +1 -1
- package/server/workspace/journal/optimizationPass.ts +4 -5
- package/server/workspace/journal/paths.ts +8 -24
- package/server/workspace/journal/state.ts +18 -8
- package/server/workspace/news/reader.ts +248 -0
- package/server/workspace/paths.ts +4 -3
- package/server/workspace/reference-dirs.ts +3 -3
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +5 -4
- package/server/workspace/skills/user-tasks.ts +3 -2
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +2 -2
- package/server/workspace/sources/classifier.ts +1 -1
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +3 -3
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +59 -13
- package/server/workspace/sources/pipeline/index.ts +59 -7
- package/server/workspace/sources/pipeline/notify.ts +13 -5
- package/server/workspace/sources/pipeline/plan.ts +11 -9
- package/server/workspace/sources/pipeline/summarize.ts +1 -1
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/sourceState.ts +9 -4
- package/server/workspace/sources/types.ts +9 -0
- package/server/workspace/sources/urls.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +4 -4
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +477 -251
- package/src/components/CanvasViewToggle.vue +12 -10
- package/src/components/ChatInput.vue +112 -105
- package/src/components/FileContentHeader.vue +10 -7
- package/src/components/FileContentRenderer.vue +37 -10
- package/src/components/FileTree.vue +34 -4
- package/src/components/FileTreePane.vue +32 -27
- package/src/components/FilesView.vue +5 -3
- package/src/components/FilterChip.vue +22 -0
- package/src/components/LockStatusPopup.vue +19 -13
- package/src/components/NewsView.vue +252 -0
- package/src/components/NotificationBell.vue +35 -9
- package/src/components/NotificationToast.vue +4 -1
- package/src/components/PageChatComposer.vue +101 -0
- package/src/components/PluginLauncher.vue +36 -62
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +3 -2
- package/src/components/SessionHeaderControls.vue +63 -0
- package/src/components/SessionHistoryExpandButton.vue +30 -0
- package/src/components/SessionHistoryPanel.vue +64 -93
- package/src/components/SessionHistoryToggleButton.vue +40 -0
- package/src/components/SessionRoleIcon.vue +72 -0
- package/src/components/SessionSidebar.vue +96 -0
- package/src/components/SessionTabBar.vue +44 -51
- package/src/components/SettingsMcpTab.vue +361 -52
- package/src/components/SettingsModal.vue +203 -72
- package/src/components/SettingsReferenceDirsTab.vue +72 -51
- package/src/components/SettingsWorkspaceDirsTab.vue +74 -51
- package/src/components/SidebarHeader.vue +50 -16
- package/src/components/SourcesManager.vue +900 -0
- package/src/components/SourcesView.vue +45 -0
- package/src/components/StackView.vue +84 -48
- package/src/components/SuggestionsPanel.vue +25 -36
- package/src/components/SystemFileBanner.vue +106 -0
- package/src/components/ThinkingIndicator.vue +41 -0
- package/src/components/TodoExplorer.vue +72 -22
- package/src/components/todo/TodoAddDialog.vue +17 -12
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +15 -10
- package/src/components/todo/TodoKanbanView.vue +16 -6
- package/src/components/todo/TodoListView.vue +14 -3
- package/src/components/todo/TodoTableView.vue +36 -5
- package/src/composables/favicon/conditions.ts +76 -0
- package/src/composables/favicon/resolveColor.ts +93 -0
- package/src/composables/favicon/types.ts +61 -0
- package/src/composables/useAppApi.ts +23 -0
- package/src/composables/useChatScroll.ts +5 -5
- package/src/composables/useCurrentRole.ts +32 -0
- package/src/composables/useDynamicFavicon.ts +174 -58
- package/src/composables/useEventListeners.ts +7 -12
- package/src/composables/useFaviconState.ts +93 -12
- package/src/composables/useFileSelection.ts +25 -6
- package/src/composables/useHealth.ts +76 -7
- package/src/composables/useLayoutMode.ts +27 -0
- package/src/composables/useNewsItems.ts +38 -0
- package/src/composables/useNewsReadState.ts +75 -0
- package/src/composables/useNotifications.ts +76 -13
- package/src/composables/usePendingCalls.ts +11 -1
- package/src/composables/useRoles.ts +6 -10
- package/src/composables/useRunElapsed.ts +80 -0
- package/src/composables/useSessionDerived.ts +21 -5
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSidePanelVisible.ts +25 -0
- package/src/composables/useViewLayout.ts +16 -37
- package/src/config/apiRoutes.ts +19 -6
- package/src/config/historyFilters.ts +30 -0
- package/src/config/mcpCatalog.ts +285 -0
- package/src/config/mcpTypes.ts +26 -0
- package/src/config/roles.ts +19 -51
- package/src/config/systemFileDescriptors.ts +170 -0
- package/src/config/toolNames.ts +6 -1
- package/src/config/workspacePaths.ts +1 -0
- package/src/index.css +14 -0
- package/src/lang/de.ts +706 -0
- package/src/lang/en.ts +726 -0
- package/src/lang/es.ts +712 -0
- package/src/lang/fr.ts +704 -0
- package/src/lang/ja.ts +707 -0
- package/src/lang/ko.ts +709 -0
- package/src/lang/pt-BR.ts +702 -0
- package/src/lang/zh.ts +705 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +3 -0
- package/src/plugins/canvas/View.vue +104 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/canvas/index.ts +3 -2
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/chart/index.ts +3 -2
- package/src/plugins/editImage/index.ts +3 -2
- package/src/plugins/generateImage/index.ts +3 -2
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +67 -46
- package/src/plugins/manageRoles/index.ts +3 -2
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +39 -34
- package/src/plugins/manageSkills/index.ts +3 -2
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/manageSource/View.vue +3 -687
- package/src/plugins/manageSource/index.ts +3 -2
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +164 -73
- package/src/plugins/markdown/definition.ts +6 -4
- package/src/plugins/markdown/index.ts +3 -2
- package/src/plugins/presentForm/Preview.vue +99 -0
- package/src/plugins/presentForm/View.vue +675 -0
- package/src/plugins/presentForm/definition.ts +127 -0
- package/src/plugins/presentForm/index.ts +18 -0
- package/src/plugins/presentForm/plugin.ts +94 -0
- package/src/plugins/presentForm/types.ts +109 -0
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/index.ts +3 -2
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +36 -26
- package/src/plugins/presentMulmoScript/index.ts +3 -2
- package/src/plugins/scheduler/AutomationsPreview.vue +37 -0
- package/src/plugins/scheduler/AutomationsView.vue +23 -0
- package/src/plugins/scheduler/CalendarView.vue +23 -0
- package/src/plugins/scheduler/LegacySchedulerView.vue +32 -0
- package/src/plugins/scheduler/Preview.vue +7 -4
- package/src/plugins/scheduler/TasksTab.vue +119 -28
- package/src/plugins/scheduler/View.vue +75 -32
- package/src/plugins/scheduler/automationsDefinition.ts +58 -0
- package/src/plugins/scheduler/calendarDefinition.ts +46 -0
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/scheduler/index.ts +68 -14
- package/src/plugins/scheduler/legacyShape.ts +34 -0
- package/src/plugins/spreadsheet/Preview.vue +9 -5
- package/src/plugins/spreadsheet/View.vue +43 -57
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +2 -1
- package/src/plugins/spreadsheet/index.ts +3 -2
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +42 -45
- package/src/plugins/textResponse/utils.ts +25 -0
- package/src/plugins/todo/Preview.vue +11 -6
- package/src/plugins/todo/View.vue +27 -13
- package/src/plugins/todo/composables/useTodos.ts +3 -1
- package/src/plugins/todo/index.ts +3 -2
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +5 -2
- package/src/plugins/wiki/View.vue +539 -92
- package/src/plugins/wiki/index.ts +5 -2
- package/src/plugins/wiki/route.ts +121 -0
- package/src/router/guards.ts +43 -24
- package/src/router/index.ts +53 -26
- package/src/router/pageRoutes.ts +23 -0
- package/src/tools/index.ts +12 -5
- package/src/tools/legacyPluginNames.ts +13 -0
- package/src/types/notification.ts +31 -6
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/eventDispatch.ts +3 -6
- package/src/utils/agent/formatElapsed.ts +37 -0
- package/src/utils/agent/request.ts +22 -1
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/canvas/sidePanelVisible.ts +19 -0
- package/src/utils/dom/scrollIntoViewByTestId.ts +38 -0
- package/src/utils/errors.ts +9 -2
- package/src/utils/files/filename.ts +24 -0
- package/src/utils/filesPreview/schedulerPreview.ts +9 -3
- package/src/utils/id.ts +18 -0
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/markdown/taskList.ts +175 -0
- package/src/utils/mcp/interpolateSpec.ts +97 -0
- package/src/utils/notification/dispatch.ts +51 -15
- package/src/utils/path/workspaceLinkRouter.ts +99 -0
- package/src/utils/session/mergeSessions.ts +5 -0
- package/src/utils/sources/filter.ts +69 -0
- package/src/vite-env.d.ts +9 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
- package/client/assets/index-Bm70FDU2.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/server/workspace/journal/linkRewrite.ts +0 -4
- package/src/components/ToolResultsPanel.vue +0 -77
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/plugins/scheduler/definition.ts +0 -57
- package/src/utils/canvas/viewMode.ts +0 -46
- package/src/utils/role/plugins.ts +0 -12
- package/src/utils/session/seedRoleDefault.ts +0 -35
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
|
@@ -11,10 +11,12 @@ import { errorMessage } from "../utils/errors.js";
|
|
|
11
11
|
import { isNonEmptyString, isRecord } from "../utils/types.js";
|
|
12
12
|
import { API_ROUTES } from "../../src/config/apiRoutes.js";
|
|
13
13
|
import { env } from "../system/env.js";
|
|
14
|
-
import { extractFetchError } from "../utils/fetch.js";
|
|
14
|
+
import { extractFetchError, fetchWithTimeout } from "../utils/fetch.js";
|
|
15
|
+
import { ONE_MINUTE_MS, ONE_SECOND_MS } from "../utils/time.js";
|
|
15
16
|
import { safeResponseText } from "../utils/http.js";
|
|
16
17
|
import { readTextSafeSync } from "../utils/files/safe.js";
|
|
17
18
|
import { WORKSPACE_PATHS } from "../workspace/paths.js";
|
|
19
|
+
import { makeUuid } from "../utils/id.js";
|
|
18
20
|
|
|
19
21
|
type JsonRpcId = string | number | null;
|
|
20
22
|
|
|
@@ -112,6 +114,25 @@ const ALL_TOOLS: Record<string, ToolDef> = {
|
|
|
112
114
|
|
|
113
115
|
const tools = PLUGIN_NAMES.map((name) => ALL_TOOLS[name]).filter(Boolean);
|
|
114
116
|
|
|
117
|
+
// MCP tools (e.g. readXPost, searchX) call external APIs through their
|
|
118
|
+
// own handlers. The bridge timeout must exceed those inner timeouts
|
|
119
|
+
// plus a small buffer for JSON parsing / HTTP round-trip, otherwise the
|
|
120
|
+
// bridge aborts before the handler can return a formatted error.
|
|
121
|
+
// Currently the slowest inner timeout is the X API (20 s); 30 s gives
|
|
122
|
+
// 10 s of headroom and still lands well inside the MCP client's own
|
|
123
|
+
// 30-60 s tool-call window.
|
|
124
|
+
const MCP_TOOL_BRIDGE_TIMEOUT_MS = 30 * ONE_SECOND_MS;
|
|
125
|
+
|
|
126
|
+
// Plugin tools (e.g. presentDocument, openCanvas) may invoke generative
|
|
127
|
+
// AI — image generation routinely takes 10–30 s per call, video can run
|
|
128
|
+
// for several minutes, and a single tool invocation can fan out to
|
|
129
|
+
// multiple parallel generations. The bridge MUST stay out of the way:
|
|
130
|
+
// set a ceiling far above any realistic completion time so the limiting
|
|
131
|
+
// factor is the agent SDK's own tool-call window, never this fetch.
|
|
132
|
+
// Pick 20 minutes — long enough for batch image gen + future video gen,
|
|
133
|
+
// short enough that a truly wedged Express handler still surfaces.
|
|
134
|
+
const PLUGIN_BRIDGE_TIMEOUT_MS = 20 * ONE_MINUTE_MS;
|
|
135
|
+
|
|
115
136
|
function respond(msg: unknown): void {
|
|
116
137
|
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
117
138
|
}
|
|
@@ -130,8 +151,42 @@ function respond(msg: unknown): void {
|
|
|
130
151
|
// Pass `allowHttpError: true` for callers that want to inspect the
|
|
131
152
|
// response themselves (e.g. /api/mcp-tools/* which has its own
|
|
132
153
|
// status-aware result handling).
|
|
154
|
+
//
|
|
155
|
+
// =====================================================================
|
|
156
|
+
// TIMEOUT POLICY — read this before adding a new bridge call
|
|
157
|
+
// =====================================================================
|
|
158
|
+
// The default `timeoutMs` from `fetchWithTimeout` is 10 s, sized for
|
|
159
|
+
// healthy localhost round-trips. THAT IS TOO SHORT for any handler
|
|
160
|
+
// that fans out to generative AI (image / video / model calls) or
|
|
161
|
+
// hits a slow external API. Whenever the downstream handler can
|
|
162
|
+
// legitimately take longer than ~5 s, the caller MUST pass an
|
|
163
|
+
// explicit `timeoutMs` that comfortably exceeds the worst realistic
|
|
164
|
+
// completion time:
|
|
165
|
+
//
|
|
166
|
+
// - Generative AI plugins (presentDocument, openCanvas, …)
|
|
167
|
+
// → use `PLUGIN_BRIDGE_TIMEOUT_MS` (20 min). Image batches and
|
|
168
|
+
// future video generation MUST NOT be limited by the bridge.
|
|
169
|
+
// - External-API MCP tools (readXPost, searchX, …)
|
|
170
|
+
// → use `MCP_TOOL_BRIDGE_TIMEOUT_MS` (30 s) or a custom value
|
|
171
|
+
// larger than the inner API's own timeout.
|
|
172
|
+
// - Pure server-state RPCs (toolResult push, role switch, …)
|
|
173
|
+
// → default 10 s is fine; these are local JSON round-trips.
|
|
174
|
+
//
|
|
175
|
+
// On EVERY failure (timeout, abort, connection reset, HTTP 5xx) this
|
|
176
|
+
// function MUST emit an error log to stderr. Silent timeouts are the
|
|
177
|
+
// exact failure mode that hid the original bug — the server kept
|
|
178
|
+
// generating images, the bridge gave up at 10 s, and the user saw
|
|
179
|
+
// "tool failed" with no trace in the logs. If you change the catch
|
|
180
|
+
// block below, keep the log emission.
|
|
181
|
+
// =====================================================================
|
|
133
182
|
interface PostJsonOpts {
|
|
134
183
|
allowHttpError?: boolean;
|
|
184
|
+
// Override the default bridge-call timeout. Needed when the
|
|
185
|
+
// downstream handler itself does slow work (e.g. /api/mcp-tools/*
|
|
186
|
+
// that hits an external API): the bridge must wait long enough for
|
|
187
|
+
// the handler's own timeout to fire, otherwise the outer abort
|
|
188
|
+
// preempts a formatted error.
|
|
189
|
+
timeoutMs?: number;
|
|
135
190
|
}
|
|
136
191
|
|
|
137
192
|
async function postJson(path: string, body: unknown, opts: PostJsonOpts = {}): Promise<Response> {
|
|
@@ -141,18 +196,33 @@ async function postJson(path: string, body: unknown, opts: PostJsonOpts = {}): P
|
|
|
141
196
|
// The path arg is used as-is because all current call sites pass
|
|
142
197
|
// hardcoded literals.
|
|
143
198
|
let res: Response;
|
|
199
|
+
const startedAt = Date.now();
|
|
144
200
|
try {
|
|
145
|
-
res = await
|
|
201
|
+
res = await fetchWithTimeout(`${BASE_URL}${path}?session=${encodeURIComponent(SESSION_ID)}`, {
|
|
146
202
|
method: "POST",
|
|
147
203
|
headers: { "Content-Type": "application/json", ...AUTH_HEADER },
|
|
148
204
|
body: JSON.stringify(body),
|
|
205
|
+
timeoutMs: opts.timeoutMs,
|
|
149
206
|
});
|
|
150
207
|
} catch (err) {
|
|
208
|
+
// `fetchWithTimeout` throws a DOMException("TimeoutError") when
|
|
209
|
+
// the timer fires; surface that case explicitly so the operator
|
|
210
|
+
// can tell "timeout vs. connection error" at a glance.
|
|
211
|
+
const elapsedMs = Date.now() - startedAt;
|
|
212
|
+
const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
|
|
213
|
+
const kind = isTimeout ? "TIMEOUT" : "NETWORK";
|
|
214
|
+
console.error(`[mcp-bridge] ${kind} ${path} after ${elapsedMs}ms (timeoutMs=${opts.timeoutMs ?? "default"}): ${errorMessage(err)}`);
|
|
151
215
|
throw new Error(`Network error calling ${path}: ${errorMessage(err)}`);
|
|
152
216
|
}
|
|
153
217
|
if (!opts.allowHttpError && !res.ok) {
|
|
154
218
|
const errBody = await safeResponseText(res, 500);
|
|
155
219
|
const detail = errBody ? `: ${errBody}` : "";
|
|
220
|
+
// Mirror the network/timeout error path above — log to stderr so
|
|
221
|
+
// an HTTP 4xx/5xx from the server never hides from the bridge
|
|
222
|
+
// operator. Without this, the thrown Error propagates silently
|
|
223
|
+
// to the MCP caller and the log stream shows nothing.
|
|
224
|
+
const elapsedMs = Date.now() - startedAt;
|
|
225
|
+
console.error(`[mcp-bridge] HTTP ${res.status} ${path} after ${elapsedMs}ms${detail}`);
|
|
156
226
|
throw new Error(`HTTP ${res.status} calling ${path}${detail}`);
|
|
157
227
|
}
|
|
158
228
|
return res;
|
|
@@ -176,7 +246,7 @@ async function fetchSkillsList(): Promise<{ name: string }[]> {
|
|
|
176
246
|
const url = `${BASE_URL}/api/skills?session=${encodeURIComponent(SESSION_ID)}`;
|
|
177
247
|
let res: Response;
|
|
178
248
|
try {
|
|
179
|
-
res = await
|
|
249
|
+
res = await fetchWithTimeout(url, { headers: AUTH_HEADER });
|
|
180
250
|
} catch (err) {
|
|
181
251
|
throw new Error(`Network error calling /api/skills: ${errorMessage(err)}`);
|
|
182
252
|
}
|
|
@@ -192,7 +262,7 @@ async function pushSkillsListResult(message: string): Promise<void> {
|
|
|
192
262
|
const skills = await fetchSkillsList();
|
|
193
263
|
await postJson(API_ROUTES.agent.internal.toolResult, {
|
|
194
264
|
toolName: "manageSkills",
|
|
195
|
-
uuid:
|
|
265
|
+
uuid: makeUuid(),
|
|
196
266
|
title: "Skills",
|
|
197
267
|
message,
|
|
198
268
|
data: { skills },
|
|
@@ -204,7 +274,7 @@ async function handleManageSkillsList(): Promise<string> {
|
|
|
204
274
|
const suffix = skills.length === 1 ? "" : "s";
|
|
205
275
|
await postJson(API_ROUTES.agent.internal.toolResult, {
|
|
206
276
|
toolName: "manageSkills",
|
|
207
|
-
uuid:
|
|
277
|
+
uuid: makeUuid(),
|
|
208
278
|
title: "Skills",
|
|
209
279
|
message: `Found ${skills.length} skill${suffix}.`,
|
|
210
280
|
data: { skills },
|
|
@@ -237,7 +307,7 @@ async function handleManageSkillsUpdate(args: Record<string, unknown>): Promise<
|
|
|
237
307
|
const url = `${BASE_URL}/api/skills/${encodeURIComponent(name)}?session=${encodeURIComponent(SESSION_ID)}`;
|
|
238
308
|
let res: Response;
|
|
239
309
|
try {
|
|
240
|
-
res = await
|
|
310
|
+
res = await fetchWithTimeout(url, {
|
|
241
311
|
method: "PUT",
|
|
242
312
|
headers: { ...AUTH_HEADER, "Content-Type": "application/json" },
|
|
243
313
|
body: JSON.stringify({
|
|
@@ -260,7 +330,7 @@ async function handleManageSkillsDelete(args: Record<string, unknown>): Promise<
|
|
|
260
330
|
const url = `/api/skills/${encodeURIComponent(name)}?session=${encodeURIComponent(SESSION_ID)}`;
|
|
261
331
|
let res: Response;
|
|
262
332
|
try {
|
|
263
|
-
res = await
|
|
333
|
+
res = await fetchWithTimeout(`${BASE_URL}${url}`, {
|
|
264
334
|
method: "DELETE",
|
|
265
335
|
headers: AUTH_HEADER,
|
|
266
336
|
});
|
|
@@ -290,7 +360,7 @@ async function handleToolCall(name: string, args: Record<string, unknown>): Prom
|
|
|
290
360
|
if (args.action === "list" && result.success) {
|
|
291
361
|
await postJson(API_ROUTES.agent.internal.toolResult, {
|
|
292
362
|
toolName: "manageRoles",
|
|
293
|
-
uuid:
|
|
363
|
+
uuid: makeUuid(),
|
|
294
364
|
...result,
|
|
295
365
|
});
|
|
296
366
|
}
|
|
@@ -303,10 +373,14 @@ async function handleToolCall(name: string, args: Record<string, unknown>): Prom
|
|
|
303
373
|
// Pure MCP tools — call via /api/mcp-tools/:tool, return text directly
|
|
304
374
|
// (no frontend push). Opt out of postJson's HTTP error throw because
|
|
305
375
|
// we want to surface the JSON error body to the caller as a string.
|
|
376
|
+
// The tool handler may hit a slow external API (e.g. X), so pass a
|
|
377
|
+
// longer bridge timeout than the default 10 s used for localhost
|
|
378
|
+
// roundtrips — see MCP_TOOL_BRIDGE_TIMEOUT_MS.
|
|
306
379
|
const mcpTool = mcpTools.find((toolDef) => toolDef.definition.name === name);
|
|
307
380
|
if (mcpTool) {
|
|
308
381
|
const res = await postJson(`/api/mcp-tools/${name}`, args, {
|
|
309
382
|
allowHttpError: true,
|
|
383
|
+
timeoutMs: MCP_TOOL_BRIDGE_TIMEOUT_MS,
|
|
310
384
|
});
|
|
311
385
|
const json = await res.json();
|
|
312
386
|
if (!res.ok) return `Error: ${json.error ?? res.status}`;
|
|
@@ -316,13 +390,17 @@ async function handleToolCall(name: string, args: Record<string, unknown>): Prom
|
|
|
316
390
|
const tool = tools.find((toolDef) => toolDef.name === name);
|
|
317
391
|
if (!tool) throw new Error(`Unknown tool: ${name}`);
|
|
318
392
|
|
|
319
|
-
|
|
393
|
+
// Plugin handlers can fan out to generative AI (image batches via
|
|
394
|
+
// presentDocument, future video). The bridge MUST wait long enough
|
|
395
|
+
// for the slowest realistic completion — see PLUGIN_BRIDGE_TIMEOUT_MS
|
|
396
|
+
// and the timeout-policy comment on `postJson`.
|
|
397
|
+
const res = await postJson(tool.endpoint!, args, { timeoutMs: PLUGIN_BRIDGE_TIMEOUT_MS });
|
|
320
398
|
const result = await res.json();
|
|
321
399
|
|
|
322
400
|
// Push visual ToolResult to the frontend via the session
|
|
323
401
|
await postJson(API_ROUTES.agent.internal.toolResult, {
|
|
324
402
|
toolName: name,
|
|
325
|
-
uuid:
|
|
403
|
+
uuid: makeUuid(),
|
|
326
404
|
...result,
|
|
327
405
|
});
|
|
328
406
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Router, Request, Response } from "express";
|
|
2
2
|
import { readXPost, searchX } from "./x.js";
|
|
3
|
+
import { notify } from "./notify.js";
|
|
3
4
|
import { errorMessage } from "../../utils/errors.js";
|
|
4
5
|
import { notFound, sendError, serverError } from "../../utils/httpError.js";
|
|
5
6
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
@@ -15,9 +16,9 @@ export interface McpTool {
|
|
|
15
16
|
handler: (args: Record<string, unknown>) => Promise<string>;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export const mcpTools: McpTool[] = [readXPost, searchX];
|
|
19
|
+
export const mcpTools: McpTool[] = [readXPost, searchX, notify];
|
|
19
20
|
|
|
20
|
-
const toolMap = new Map(mcpTools.map((
|
|
21
|
+
const toolMap = new Map(mcpTools.map((tool) => [tool.definition.name, tool]));
|
|
21
22
|
|
|
22
23
|
export function isMcpToolEnabled(tool: McpTool): boolean {
|
|
23
24
|
return (tool.requiredEnv ?? []).every((key) => !!process.env[key]);
|
|
@@ -34,11 +35,11 @@ interface McpToolParams {
|
|
|
34
35
|
// GET /api/mcp-tools — returns { name, enabled, requiredEnv } for each tool (used by the role builder UI)
|
|
35
36
|
mcpToolsRouter.get(API_ROUTES.mcpTools.list, (_req: Request, res: Response) => {
|
|
36
37
|
res.json(
|
|
37
|
-
mcpTools.map((
|
|
38
|
-
name:
|
|
39
|
-
enabled: isMcpToolEnabled(
|
|
40
|
-
requiredEnv:
|
|
41
|
-
prompt:
|
|
38
|
+
mcpTools.map((tool) => ({
|
|
39
|
+
name: tool.definition.name,
|
|
40
|
+
enabled: isMcpToolEnabled(tool),
|
|
41
|
+
requiredEnv: tool.requiredEnv ?? [],
|
|
42
|
+
prompt: tool.prompt,
|
|
42
43
|
})),
|
|
43
44
|
);
|
|
44
45
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// `notify` MCP tool — exposes the server's notification bus to the
|
|
2
|
+
// agent so the user can ask "通知して" / "monitor the build and tell
|
|
3
|
+
// me when it's done" and the agent has a direct way to fire.
|
|
4
|
+
//
|
|
5
|
+
// Calls `publishNotification` with `kind: "push"`, which fans out
|
|
6
|
+
// to:
|
|
7
|
+
// - Web bell
|
|
8
|
+
// - Bridge (if a transportId is supplied — N/A from this entry
|
|
9
|
+
// point)
|
|
10
|
+
// - macOS Reminders (#789, on darwin unless the user has set
|
|
11
|
+
// DISABLE_MACOS_REMINDER_NOTIFICATIONS=1)
|
|
12
|
+
//
|
|
13
|
+
// No active-user gate. If the user asks for a notification, fire it.
|
|
14
|
+
//
|
|
15
|
+
// `body` is optional and only forwarded when non-empty. `title` is
|
|
16
|
+
// required and trimmed.
|
|
17
|
+
//
|
|
18
|
+
// `publishNotification` is injected via `makeNotifyTool({ publish })`
|
|
19
|
+
// (#803) so unit tests can pass a mock and stay free of macOS / bell
|
|
20
|
+
// side effects. The default singleton `notify` wires the real
|
|
21
|
+
// implementation.
|
|
22
|
+
|
|
23
|
+
import { publishNotification } from "../../events/notifications.js";
|
|
24
|
+
import { NOTIFICATION_KINDS } from "../../../src/types/notification.js";
|
|
25
|
+
|
|
26
|
+
export type NotifyPublishFn = typeof publishNotification;
|
|
27
|
+
|
|
28
|
+
export interface NotifyToolDeps {
|
|
29
|
+
publish: NotifyPublishFn;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function makeNotifyTool(deps: NotifyToolDeps) {
|
|
33
|
+
return {
|
|
34
|
+
definition: {
|
|
35
|
+
name: "notify",
|
|
36
|
+
description:
|
|
37
|
+
"Send the user a push-style notification (web bell + macOS Reminders if MACOS_REMINDER_NOTIFICATIONS=1 + bridge). Use to report completion of long-running tasks, surface monitoring results, or proactively notify the user when they may be away from the keyboard.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
title: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Short notification headline. Keep it concise — emojis OK.",
|
|
44
|
+
},
|
|
45
|
+
body: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Optional longer detail line. Omit when the title is self-explanatory.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ["title"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
prompt:
|
|
55
|
+
"Use the `notify` MCP tool — NOT a user-installed `/notify` skill — when the user asks for a notification ('通知して' / 'remind me' / 'tell me when …') or when reporting completion of a long-running task / monitoring summary / scheduled reminder firing. " +
|
|
56
|
+
"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. " +
|
|
57
|
+
"After firing, briefly tell the user you sent the notification.",
|
|
58
|
+
|
|
59
|
+
async handler(args: Record<string, unknown>): Promise<string> {
|
|
60
|
+
const title = typeof args.title === "string" ? args.title.trim() : "";
|
|
61
|
+
if (!title) return "notify: `title` is required (non-empty string).";
|
|
62
|
+
const bodyRaw = typeof args.body === "string" ? args.body.trim() : "";
|
|
63
|
+
const body = bodyRaw.length > 0 ? bodyRaw : undefined;
|
|
64
|
+
|
|
65
|
+
deps.publish({
|
|
66
|
+
kind: NOTIFICATION_KINDS.push,
|
|
67
|
+
title,
|
|
68
|
+
body,
|
|
69
|
+
});
|
|
70
|
+
return body ? `Notification sent: ${title}\n${body}` : `Notification sent: ${title}`;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Production singleton wired with the real publishNotification.
|
|
76
|
+
export const notify = makeNotifyTool({ publish: publishNotification });
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { errorMessage } from "../../utils/errors.js";
|
|
2
|
+
import { fetchWithTimeout } from "../../utils/fetch.js";
|
|
2
3
|
import { safeResponseText } from "../../utils/http.js";
|
|
4
|
+
import { toUtcIsoDate } from "../../utils/date.js";
|
|
5
|
+
import { ONE_SECOND_MS } from "../../utils/time.js";
|
|
3
6
|
import { env } from "../../system/env.js";
|
|
4
7
|
|
|
5
8
|
const X_API_BASE = "https://api.twitter.com/2";
|
|
9
|
+
|
|
10
|
+
// X API can stall under rate limit — a 10 s default (used for internal
|
|
11
|
+
// localhost calls) would produce false timeouts. 20 s gives enough
|
|
12
|
+
// headroom for a slow but real response while still bailing long
|
|
13
|
+
// before the MCP client's tool-call timeout fires.
|
|
14
|
+
const X_API_TIMEOUT_MS = 20 * ONE_SECOND_MS;
|
|
6
15
|
const TWEET_FIELDS = "tweet.fields=created_at,author_id,public_metrics,entities";
|
|
7
16
|
const EXPANSIONS = "expansions=author_id";
|
|
8
17
|
const USER_FIELDS = "user.fields=name,username";
|
|
@@ -38,8 +47,9 @@ async function fetchX(path: string): Promise<XApiResponse> {
|
|
|
38
47
|
|
|
39
48
|
let response: Response;
|
|
40
49
|
try {
|
|
41
|
-
response = await
|
|
50
|
+
response = await fetchWithTimeout(`${X_API_BASE}${path}`, {
|
|
42
51
|
headers: { Authorization: `Bearer ${token}` },
|
|
52
|
+
timeoutMs: X_API_TIMEOUT_MS,
|
|
43
53
|
});
|
|
44
54
|
} catch (err) {
|
|
45
55
|
throw new Error(`Network error calling X API: ${errorMessage(err)}`);
|
|
@@ -56,7 +66,7 @@ async function fetchX(path: string): Promise<XApiResponse> {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
function formatTweet(tweet: XTweet, author?: XUser, url?: string): string {
|
|
59
|
-
const date = tweet.created_at ? new Date(tweet.created_at)
|
|
69
|
+
const date = tweet.created_at ? toUtcIsoDate(new Date(tweet.created_at)) : "";
|
|
60
70
|
const dateSuffix = date ? ` · ${date}` : "";
|
|
61
71
|
const byline = author ? `@${author.username} (${author.name})${dateSuffix}` : date;
|
|
62
72
|
const metrics = tweet.public_metrics
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import TodoDef from "../../src/plugins/todo/definition.js";
|
|
8
|
-
import
|
|
8
|
+
import ManageCalendarDef from "../../src/plugins/scheduler/calendarDefinition.js";
|
|
9
|
+
import ManageAutomationsDef from "../../src/plugins/scheduler/automationsDefinition.js";
|
|
9
10
|
import PresentMulmoScriptDef from "../../src/plugins/presentMulmoScript/definition.js";
|
|
10
11
|
import ManageRolesDef from "../../src/plugins/manageRoles/definition.js";
|
|
11
12
|
import ManageSkillsDef from "../../src/plugins/manageSkills/definition.js";
|
|
@@ -18,7 +19,7 @@ import SpreadsheetDef from "../../src/plugins/spreadsheet/definition.js";
|
|
|
18
19
|
import { TOOL_DEFINITION as MindMapDef } from "@gui-chat-plugin/mindmap";
|
|
19
20
|
import GenerateImageDef from "../../src/plugins/generateImage/definition.js";
|
|
20
21
|
import { TOOL_DEFINITION as QuizDef } from "@mulmochat-plugin/quiz";
|
|
21
|
-
import { TOOL_DEFINITION as FormDef } from "
|
|
22
|
+
import { TOOL_DEFINITION as FormDef } from "../../src/plugins/presentForm/definition.js";
|
|
22
23
|
import CanvasDef from "../../src/plugins/canvas/definition.js";
|
|
23
24
|
import EditImageDef from "../../src/plugins/editImage/definition.js";
|
|
24
25
|
import { TOOL_DEFINITION as Present3DDef } from "@gui-chat-plugin/present3d";
|
|
@@ -27,7 +28,11 @@ import { API_ROUTES } from "../../src/config/apiRoutes.js";
|
|
|
27
28
|
/** Maps plugin tool name → REST API endpoint. */
|
|
28
29
|
export const TOOL_ENDPOINTS: Record<string, string> = {
|
|
29
30
|
[TodoDef.name]: API_ROUTES.todos.dispatch,
|
|
30
|
-
|
|
31
|
+
// Both halves of the former manageScheduler share the same backend
|
|
32
|
+
// endpoint (#824 / PR #758). Server-side dispatch routes per-action
|
|
33
|
+
// via TASK_ACTIONS, so one route happily handles both tool names.
|
|
34
|
+
[ManageCalendarDef.name]: API_ROUTES.scheduler.base,
|
|
35
|
+
[ManageAutomationsDef.name]: API_ROUTES.scheduler.base,
|
|
31
36
|
[MarkdownDef.name]: API_ROUTES.plugins.presentDocument,
|
|
32
37
|
[SpreadsheetDef.name]: API_ROUTES.plugins.presentSpreadsheet,
|
|
33
38
|
[MindMapDef.name]: API_ROUTES.plugins.mindmap,
|
|
@@ -49,7 +54,8 @@ export const TOOL_ENDPOINTS: Record<string, string> = {
|
|
|
49
54
|
/** All ToolDefinition objects for package and local plugins. */
|
|
50
55
|
export const PLUGIN_DEFS = [
|
|
51
56
|
TodoDef,
|
|
52
|
-
|
|
57
|
+
ManageCalendarDef,
|
|
58
|
+
ManageAutomationsDef,
|
|
53
59
|
PresentMulmoScriptDef,
|
|
54
60
|
MarkdownDef,
|
|
55
61
|
SpreadsheetDef,
|
package/server/agent/prompt.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { WORKSPACE_DIRS, WORKSPACE_FILES } from "../workspace/paths.js";
|
|
|
7
7
|
import { getCachedCustomDirs, buildCustomDirsPrompt } from "../workspace/custom-dirs.js";
|
|
8
8
|
import { TOOL_NAMES } from "../../src/config/toolNames.js";
|
|
9
9
|
import { getCachedReferenceDirs, buildReferenceDirsPrompt } from "../workspace/reference-dirs.js";
|
|
10
|
+
import { log } from "../system/logger/index.js";
|
|
11
|
+
import { toLocalIsoDate } from "../utils/date.js";
|
|
10
12
|
|
|
11
13
|
export const SYSTEM_PROMPT = `You are MulmoClaude, a versatile assistant app with rich visual output.
|
|
12
14
|
|
|
@@ -242,6 +244,35 @@ export function buildNewsConciergeContext(role: Role): string | null {
|
|
|
242
244
|
return NEWS_CONCIERGE_PROMPT;
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
// Single-paragraph prompts up to this length collapse into a compact
|
|
248
|
+
// `- **name**: body` bullet instead of the old `### name\n\n body`
|
|
249
|
+
// heading. Saves ~25 chars of heading overhead per plugin and keeps the
|
|
250
|
+
// whole "Plugin Instructions" block scannable. Multi-paragraph or
|
|
251
|
+
// longer prompts keep the heading form so the structure is preserved.
|
|
252
|
+
const PLUGIN_COMPACT_MAX_CHARS = 400;
|
|
253
|
+
|
|
254
|
+
export function formatPluginSection(name: string, prompt: string): string {
|
|
255
|
+
// Normalize CRLF → LF first: a prompt authored on Windows would
|
|
256
|
+
// otherwise hide its paragraph break inside `\r\n\r\n` and the
|
|
257
|
+
// `includes("\n\n")` check would falsely classify it as single-paragraph,
|
|
258
|
+
// collapsing a multi-paragraph prompt into one bullet.
|
|
259
|
+
const normalized = prompt.replace(/\r\n/g, "\n");
|
|
260
|
+
const trimmed = normalized.trim();
|
|
261
|
+
const isSingleParagraph = !trimmed.includes("\n\n");
|
|
262
|
+
if (isSingleParagraph && trimmed.length <= PLUGIN_COMPACT_MAX_CHARS) {
|
|
263
|
+
// Flatten any single newlines inside the paragraph so the bullet
|
|
264
|
+
// stays on one visual line. Split-join avoids the super-linear
|
|
265
|
+
// backtracking that `\s*\n\s*` would bring (sonarjs/slow-regex).
|
|
266
|
+
const oneLine = trimmed
|
|
267
|
+
.split("\n")
|
|
268
|
+
.map((line) => line.trim())
|
|
269
|
+
.filter((line) => line.length > 0)
|
|
270
|
+
.join(" ");
|
|
271
|
+
return `- **${name}**: ${oneLine}`;
|
|
272
|
+
}
|
|
273
|
+
return `### ${name}\n\n${trimmed}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
245
276
|
export function buildPluginPromptSections(role: Role): string[] {
|
|
246
277
|
// Widen to Set<string> so the `.has()` checks accept arbitrary
|
|
247
278
|
// definition names (PLUGIN_DEFS entries and MCP tool names are
|
|
@@ -268,7 +299,7 @@ export function buildPluginPromptSections(role: Role): string[] {
|
|
|
268
299
|
|
|
269
300
|
// MCP tool prompts override definition prompts if both exist
|
|
270
301
|
const merged = { ...defPrompts, ...mcpToolPrompts };
|
|
271
|
-
return Object.entries(merged).map(([name, prompt]) =>
|
|
302
|
+
return Object.entries(merged).map(([name, prompt]) => formatPluginSection(name, prompt));
|
|
272
303
|
}
|
|
273
304
|
|
|
274
305
|
export interface SystemPromptParams {
|
|
@@ -279,6 +310,61 @@ export interface SystemPromptParams {
|
|
|
279
310
|
* environment has no such guarantees, so without Docker we stay
|
|
280
311
|
* silent. */
|
|
281
312
|
useDocker: boolean;
|
|
313
|
+
/** IANA timezone from the user's browser (e.g. "Asia/Tokyo"). When
|
|
314
|
+
* present, drives the time-section instruction that tells the
|
|
315
|
+
* agent to interpret bare times in that zone without asking the
|
|
316
|
+
* user every turn. Missing or invalid values fall back to
|
|
317
|
+
* server-local date only. */
|
|
318
|
+
userTimezone?: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Accept IANA-looking strings only. Anything else (including
|
|
322
|
+
// line-break injection attempts from a malicious client) is rejected
|
|
323
|
+
// and the prompt falls back to the server-local form.
|
|
324
|
+
const IANA_TZ_RE = /^[A-Za-z][A-Za-z0-9_+/-]{0,63}$/;
|
|
325
|
+
function sanitizeUserTimezone(zoneId: string | undefined): string | undefined {
|
|
326
|
+
if (typeof zoneId !== "string") return undefined;
|
|
327
|
+
if (!IANA_TZ_RE.test(zoneId)) return undefined;
|
|
328
|
+
try {
|
|
329
|
+
// Throws a RangeError if the zone isn't recognized by the ICU
|
|
330
|
+
// data on this runtime.
|
|
331
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zoneId });
|
|
332
|
+
return zoneId;
|
|
333
|
+
} catch {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function formatDateInTimezone(date: Date, zoneId: string): string | null {
|
|
339
|
+
try {
|
|
340
|
+
// en-CA gives us YYYY-MM-DD directly, matching the rest of the
|
|
341
|
+
// workspace's date convention.
|
|
342
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
343
|
+
timeZone: zoneId,
|
|
344
|
+
year: "numeric",
|
|
345
|
+
month: "2-digit",
|
|
346
|
+
day: "2-digit",
|
|
347
|
+
}).format(date);
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Compact prompt section that tells the agent (a) today's date in the
|
|
354
|
+
// user's zone and (b) not to pester the user about timezones for every
|
|
355
|
+
// bare time expression. Falls back to server-local date (previous
|
|
356
|
+
// behaviour) when the browser didn't give us a valid zone.
|
|
357
|
+
export function buildTimeSection(now: Date, userTimezone: string | undefined): string {
|
|
358
|
+
const sanitized = sanitizeUserTimezone(userTimezone);
|
|
359
|
+
if (!sanitized) {
|
|
360
|
+
return `Today's date: ${toLocalIsoDate(now)}`;
|
|
361
|
+
}
|
|
362
|
+
const today = formatDateInTimezone(now, sanitized) ?? toLocalIsoDate(now);
|
|
363
|
+
return `## Time & Timezone
|
|
364
|
+
|
|
365
|
+
The user's browser timezone is ${sanitized}. Today's date in that timezone is ${today}.
|
|
366
|
+
|
|
367
|
+
When the user mentions a time without explicitly naming a city or timezone, assume their local timezone (${sanitized}) and proceed — do NOT ask for clarification. Only confirm when the user explicitly mentions another location or timezone (e.g. "3pm in New York", "JST", "UTC+5").`;
|
|
282
368
|
}
|
|
283
369
|
|
|
284
370
|
// Mirror the tool set installed by Dockerfile.sandbox. Kept here so a
|
|
@@ -295,7 +381,47 @@ The bash tool runs inside a Docker sandbox. The following tools are guaranteed p
|
|
|
295
381
|
|
|
296
382
|
Runtime \`pip install\` / \`apt install\` are not available (no network-installed deps by design). Work within the list above; if something is missing, say so rather than attempting to install it.`;
|
|
297
383
|
|
|
298
|
-
|
|
384
|
+
// Files ≤ this threshold stay inlined verbatim; above it, only a short
|
|
385
|
+
// summary + pointer reaches the system prompt and the full content is
|
|
386
|
+
// fetched on demand via the Read tool. 2000 chars keeps today's small
|
|
387
|
+
// helps (github.md ~1.2K, spreadsheet.md ~1.4K) inline, while wiki.md /
|
|
388
|
+
// mulmoscript.md / telegram.md (4–7K each) switch to summary mode. See
|
|
389
|
+
// plans/done/feat-help-pointer-threshold.md and issue #487.
|
|
390
|
+
const HELP_INLINE_THRESHOLD_CHARS = 2000;
|
|
391
|
+
const HELP_SUMMARY_PARAGRAPH_CAP = 200;
|
|
392
|
+
|
|
393
|
+
// Pull a short, prompt-friendly summary from a help file:
|
|
394
|
+
// - first H1 heading (identifies the file)
|
|
395
|
+
// - first non-empty, non-heading paragraph, truncated to ~200 chars
|
|
396
|
+
// No frontmatter required — the goal is zero ceremony for help authors.
|
|
397
|
+
export function summarizeHelpContent(content: string): string {
|
|
398
|
+
const lines = content.split("\n");
|
|
399
|
+
const heading = lines
|
|
400
|
+
.find((line) => /^#\s+\S/.test(line))
|
|
401
|
+
?.replace(/^#\s+/, "")
|
|
402
|
+
.trim();
|
|
403
|
+
|
|
404
|
+
let paragraph = "";
|
|
405
|
+
for (const line of lines) {
|
|
406
|
+
const trimmed = line.trim();
|
|
407
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
408
|
+
if (paragraph) break;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
paragraph = paragraph ? `${paragraph} ${trimmed}` : trimmed;
|
|
412
|
+
if (paragraph.length >= HELP_SUMMARY_PARAGRAPH_CAP) break;
|
|
413
|
+
}
|
|
414
|
+
if (paragraph.length > HELP_SUMMARY_PARAGRAPH_CAP) {
|
|
415
|
+
paragraph = paragraph.slice(0, HELP_SUMMARY_PARAGRAPH_CAP).trimEnd() + "…";
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const parts: string[] = [];
|
|
419
|
+
if (heading) parts.push(heading);
|
|
420
|
+
if (paragraph) parts.push(paragraph);
|
|
421
|
+
return parts.join(" — ");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function buildInlinedHelpFiles(rolePrompt: string, workspacePath: string): string[] {
|
|
299
425
|
// Match either legacy `helps/<name>.md` or post-#284
|
|
300
426
|
// `config/helps/<name>.md` references in role prompts. Both
|
|
301
427
|
// resolve to the same on-disk file under `config/helps/`.
|
|
@@ -310,10 +436,17 @@ function buildInlinedHelpFiles(rolePrompt: string, workspacePath: string): strin
|
|
|
310
436
|
const fullPath = join(workspacePath, WORKSPACE_DIRS.helps, name);
|
|
311
437
|
if (!existsSync(fullPath)) return null;
|
|
312
438
|
const content = readFileSync(fullPath, "utf-8").trim();
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
// Read() the stale legacy location.
|
|
316
|
-
|
|
439
|
+
if (!content) return null;
|
|
440
|
+
// Keep the heading anchored to the canonical post-#284 path so
|
|
441
|
+
// the LLM can't accidentally Read() the stale legacy location.
|
|
442
|
+
const canonicalPath = `${WORKSPACE_DIRS.helps}/${name}`;
|
|
443
|
+
const header = `### ${canonicalPath}`;
|
|
444
|
+
if (content.length <= HELP_INLINE_THRESHOLD_CHARS) {
|
|
445
|
+
return `${header}\n\n${content}`;
|
|
446
|
+
}
|
|
447
|
+
const summary = summarizeHelpContent(content);
|
|
448
|
+
const pointer = `Detailed reference: use Read on \`${canonicalPath}\` when you need the full content.`;
|
|
449
|
+
return summary ? `${header}\n\n${summary}\n\n${pointer}` : `${header}\n\n${pointer}`;
|
|
317
450
|
})
|
|
318
451
|
.filter((section): section is string => section !== null);
|
|
319
452
|
}
|
|
@@ -328,27 +461,55 @@ export function headingSection(heading: string, items: string[]): string | null
|
|
|
328
461
|
return `## ${heading}\n\n${items.join("\n\n")}`;
|
|
329
462
|
}
|
|
330
463
|
|
|
464
|
+
// Named sections so buildSystemPrompt can log a size breakdown
|
|
465
|
+
// without inventing labels at the call site.
|
|
466
|
+
interface NamedSection {
|
|
467
|
+
name: string;
|
|
468
|
+
content: string | null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// System prompt above this total size gets a warning in the log —
|
|
472
|
+
// 20K chars is ~5K tokens, a noticeable slice of the context budget
|
|
473
|
+
// and a useful early-warning threshold. Doesn't block, just flags.
|
|
474
|
+
const SYSTEM_PROMPT_WARN_THRESHOLD_CHARS = 20000;
|
|
475
|
+
|
|
331
476
|
export function buildSystemPrompt(params: SystemPromptParams): string {
|
|
332
|
-
const { role, workspacePath, useDocker } = params;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
buildReferenceDirsPrompt(getCachedReferenceDirs(), useDocker),
|
|
349
|
-
headingSection("Reference Files", buildInlinedHelpFiles(role.prompt, workspacePath)),
|
|
350
|
-
headingSection("Plugin Instructions", buildPluginPromptSections(role)),
|
|
477
|
+
const { role, workspacePath, useDocker, userTimezone } = params;
|
|
478
|
+
|
|
479
|
+
const sections: NamedSection[] = [
|
|
480
|
+
{ name: "base", content: SYSTEM_PROMPT },
|
|
481
|
+
{ name: "role", content: role.prompt },
|
|
482
|
+
{ name: "workspace", content: `Workspace directory: ${workspacePath}` },
|
|
483
|
+
{ name: "time", content: buildTimeSection(new Date(), userTimezone) },
|
|
484
|
+
{ name: "memory", content: buildMemoryContext(workspacePath) },
|
|
485
|
+
{ name: "sandbox", content: useDocker ? SANDBOX_TOOLS_HINT : null },
|
|
486
|
+
{ name: "wiki", content: buildWikiContext(workspacePath) },
|
|
487
|
+
{ name: "sources", content: buildSourcesContext(workspacePath) },
|
|
488
|
+
{ name: "news-concierge", content: buildNewsConciergeContext(role) },
|
|
489
|
+
{ name: "custom-dirs", content: buildCustomDirsPrompt(getCachedCustomDirs()) },
|
|
490
|
+
{ name: "reference-dirs", content: buildReferenceDirsPrompt(getCachedReferenceDirs(), useDocker) },
|
|
491
|
+
{ name: "helps", content: headingSection("Reference Files", buildInlinedHelpFiles(role.prompt, workspacePath)) },
|
|
492
|
+
{ name: "plugins", content: headingSection("Plugin Instructions", buildPluginPromptSections(role)) },
|
|
351
493
|
];
|
|
352
494
|
|
|
353
|
-
|
|
495
|
+
const kept = sections.filter((section): section is NamedSection & { content: string } => section.content !== null);
|
|
496
|
+
const result = kept.map((section) => section.content).join("\n\n");
|
|
497
|
+
|
|
498
|
+
// Log a size breakdown so prompt-bloat regressions show up in
|
|
499
|
+
// normal run logs. Warn tier fires for outright large prompts;
|
|
500
|
+
// the debug tier gives the per-section counts for when the
|
|
501
|
+
// warning hits (or just when someone wants a baseline).
|
|
502
|
+
const breakdown = kept.map((section) => `${section.name}=${section.content.length}`).join(" ");
|
|
503
|
+
const total = result.length;
|
|
504
|
+
log.debug("prompt", "system-prompt size", { total, breakdown, roleId: role.id });
|
|
505
|
+
if (total >= SYSTEM_PROMPT_WARN_THRESHOLD_CHARS) {
|
|
506
|
+
log.warn("prompt", "system-prompt exceeds warn threshold", {
|
|
507
|
+
total,
|
|
508
|
+
threshold: SYSTEM_PROMPT_WARN_THRESHOLD_CHARS,
|
|
509
|
+
breakdown,
|
|
510
|
+
roleId: role.id,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return result;
|
|
354
515
|
}
|