mulmoclaude 0.6.2 → 0.6.3
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/README.md +26 -0
- package/bin/mulmoclaude.js +11 -1
- package/client/assets/chunk-D8eiyYIV-CW0rPbG2.js +1 -0
- package/client/assets/{html2canvas-CDGcmOD3-Bkf2uOth.js → html2canvas-CDGcmOD3-BjwfzAN8.js} +1 -1
- package/client/assets/index-Bp1owZ-i.js +5101 -0
- package/client/assets/index-c63H1pnd.css +2 -0
- package/client/assets/{index.es-DqtpmBm8-D9mAh_KQ.js → index.es-DqtpmBm8-DudYPW7R.js} +1 -1
- package/client/assets/material-symbols-outlined-C0dZ3SlO.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-BUk5WXSy.js +1 -0
- package/client/assets/{runtime-vue-BVUzgYGA.js → runtime-vue-fFYhnNg3.js} +1 -1
- package/client/assets/{vue-C8UuIO9J.js → vue-Kqzpl9Vx.js} +1 -1
- package/client/assets/vue.runtime.esm-bundler-BTyIdNAI.js +4 -0
- package/client/index.html +9 -11
- package/package.json +5 -4
- package/server/agent/backend/claude-code.ts +34 -0
- package/server/agent/backend/fake-echo.ts +370 -0
- package/server/agent/backend/index.ts +16 -1
- package/server/agent/config.ts +8 -1
- package/server/agent/mcpFailureMonitor.ts +167 -0
- package/server/agent/mcpPreflight.ts +185 -0
- package/server/agent/stream.ts +12 -1
- package/server/api/routes/mulmo-script.ts +19 -1
- package/server/api/routes/schedulerHandlers.ts +52 -4
- package/server/api/routes/sessions.ts +15 -0
- package/server/api/routes/skills.ts +263 -0
- package/server/events/notifications.ts +19 -91
- package/server/index.ts +87 -9
- package/server/notifier/macosReminderAdapter.ts +30 -0
- package/server/system/announceOptionalDeps.ts +50 -0
- package/server/system/config.ts +8 -1
- package/server/system/docker.ts +14 -6
- package/server/system/env.ts +18 -5
- package/server/system/optionalDeps.ts +129 -0
- package/server/utils/cli-flags.d.mts +14 -0
- package/server/utils/cli-flags.mjs +53 -0
- package/server/utils/time.ts +6 -0
- package/server/workspace/helps/business.md +2 -2
- package/server/workspace/helps/mulmoscript.md +3 -3
- package/server/workspace/helps/sandbox.md +2 -2
- package/server/workspace/hooks/dispatcher.mjs +1 -1
- package/server/workspace/paths.ts +13 -4
- package/server/workspace/skills/catalog.ts +355 -0
- package/server/workspace/skills/external/catalog.ts +283 -0
- package/server/workspace/skills/external/clone.ts +129 -0
- package/server/workspace/skills/external/id.ts +194 -0
- package/server/workspace/skills/external/install.ts +417 -0
- package/server/workspace/skills/external/presets.ts +50 -0
- package/server/workspace/skills-preset.ts +29 -17
- package/server/workspace/workspace.ts +10 -5
- package/src/App.vue +19 -8
- package/src/components/RightSidebar.vue +19 -0
- package/src/components/StackView.vue +10 -1
- package/src/config/apiRoutes.ts +0 -6
- package/src/config/roles.ts +2 -0
- package/src/lang/de.ts +50 -1
- package/src/lang/en.ts +49 -1
- package/src/lang/es.ts +49 -1
- package/src/lang/fr.ts +49 -1
- package/src/lang/ja.ts +49 -1
- package/src/lang/ko.ts +49 -1
- package/src/lang/pt-BR.ts +49 -1
- package/src/lang/zh.ts +49 -1
- package/src/plugins/manageSkills/View.vue +795 -30
- package/src/plugins/manageSkills/categories.ts +125 -0
- package/src/plugins/manageSkills/meta.ts +30 -0
- package/src/plugins/markdown/definition.ts +3 -3
- package/src/plugins/meta-types.ts +5 -0
- package/src/plugins/presentMulmoScript/Preview.vue +3 -3
- package/src/plugins/presentMulmoScript/View.vue +157 -33
- package/src/plugins/presentMulmoScript/meta.ts +4 -0
- package/src/plugins/scheduler/View.vue +45 -9
- package/src/plugins/scheduler/calendarDefinition.ts +6 -2
- package/src/plugins/scheduler/multiDayHelpers.ts +95 -0
- package/src/plugins/spreadsheet/View.vue +3 -3
- package/src/types/notification.ts +1 -1
- package/src/types/session.ts +6 -0
- package/src/types/sse.ts +5 -0
- package/src/types/toolCallHistory.ts +7 -0
- package/src/utils/agent/eventDispatch.ts +26 -5
- package/src/utils/agent/mcpHint.ts +50 -0
- package/src/utils/session/sessionEntries.ts +8 -32
- package/client/assets/PluginScopedRoot-YjvQq0Nn.js +0 -3
- package/client/assets/chunk-CernVdwh.js +0 -1
- package/client/assets/chunk-D8eiyYIV-CAXpUwLd.js +0 -1
- package/client/assets/index-BwrlMMHr.js +0 -5005
- package/client/assets/index-CvvNuegU.css +0 -2
- package/client/assets/material-symbols-outlined-BOZVWuR3.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-C1To4M3t.js +0 -1
- package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +0 -4
- package/server/api/routes/notifications.ts +0 -195
- package/server/notifier/legacy-adapters.ts +0 -76
- package/src/composables/useSelectedResult.ts +0 -49
- /package/client/assets/{purify.es-Fx1Nqyry-Dwtk-9WZ.js → purify.es-Fx1Nqyry-B3aL7Uvj.js} +0 -0
- /package/client/assets/{typeof-DBp4T-Ny-CSr8wx1e.js → typeof-DBp4T-Ny-Bef7RiR_.js} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// wrapper over the new notifier engine (PR 4 of feat-encore).
|
|
3
3
|
//
|
|
4
4
|
// The signature is preserved so the existing host call sites
|
|
5
|
-
// (`server/agent/mcp-tools/notify.ts`,
|
|
5
|
+
// (`server/agent/mcp-tools/notify.ts`,
|
|
6
6
|
// `server/workspace/sources/pipeline/notify.ts`,
|
|
7
7
|
// `server/plugins/diagnostics.ts`) keep working without source changes.
|
|
8
8
|
// Internally it now:
|
|
@@ -16,14 +16,17 @@
|
|
|
16
16
|
// legacy entries publish with `lifecycle: "fyi"`, so the
|
|
17
17
|
// `action`-lifecycle rules don't apply and clicking still routes).
|
|
18
18
|
// 4. Stashes the legacy fields (`kind`, `priority`, `action`,
|
|
19
|
-
// `i18n`, `
|
|
20
|
-
// `
|
|
21
|
-
//
|
|
19
|
+
// `i18n`, `sessionId`, the caller-supplied dedup `id`) on
|
|
20
|
+
// `pluginData` so the bell can preserve icon, i18n localization,
|
|
21
|
+
// and dedup.
|
|
22
22
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// `
|
|
23
|
+
// macOS Reminder push happens via the `macosReminderAdapter` listener
|
|
24
|
+
// subscribed to the notifier pubsub channel. The previous bridge
|
|
25
|
+
// fan-out path was removed (#1351 follow-up): the only callers
|
|
26
|
+
// setting `transportId` were the PoC `/api/notifications/test` route
|
|
27
|
+
// and `scheduleTestNotification`, both deleted in the same change.
|
|
28
|
+
// Production callers never set `transportId`, so the entire bridge
|
|
29
|
+
// side-channel was dead code.
|
|
27
30
|
|
|
28
31
|
import { PAGE_ROUTES } from "../../src/router/pageRoutes.js";
|
|
29
32
|
import {
|
|
@@ -38,7 +41,6 @@ import {
|
|
|
38
41
|
} from "../../src/types/notification.js";
|
|
39
42
|
import { publish as notifierPublish } from "../notifier/engine.js";
|
|
40
43
|
import type { NotifierSeverity } from "../notifier/types.js";
|
|
41
|
-
import { ONE_SECOND_MS, MAX_NOTIFICATION_DELAY_SEC } from "../utils/time.js";
|
|
42
44
|
import { log } from "../system/logger/index.js";
|
|
43
45
|
import { makeUuid } from "../utils/id.js";
|
|
44
46
|
|
|
@@ -51,7 +53,6 @@ export interface PublishNotificationOpts {
|
|
|
51
53
|
action?: NotificationAction;
|
|
52
54
|
priority?: NotificationPriority;
|
|
53
55
|
sessionId?: string;
|
|
54
|
-
transportId?: string;
|
|
55
56
|
/** Override the auto-generated UUID with a caller-supplied stable
|
|
56
57
|
* id. Used by the plugin-meta diagnostics: the same diagnostic
|
|
57
58
|
* id is returned from `/api/plugins/diagnostics`, and `pluginData`
|
|
@@ -60,7 +61,7 @@ export interface PublishNotificationOpts {
|
|
|
60
61
|
id?: string;
|
|
61
62
|
/** vue-i18n keys + params for clients to localize the title/body.
|
|
62
63
|
* Server-side `title` / `body` stay set as English fallbacks for
|
|
63
|
-
* logs
|
|
64
|
+
* logs and the macOS Reminder push. */
|
|
64
65
|
i18n?: NotificationI18n;
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -79,7 +80,6 @@ export interface LegacyNotifierPluginData {
|
|
|
79
80
|
priority: NotificationPriority;
|
|
80
81
|
action: NotificationAction;
|
|
81
82
|
i18n?: NotificationI18n;
|
|
82
|
-
transportId?: string;
|
|
83
83
|
sessionId?: string;
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -155,9 +155,7 @@ function buildChatTarget(target: Extract<NavigateTarget, { view: typeof NOTIFICA
|
|
|
155
155
|
// redirect is worse UX than a non-clickable entry. Dot-segment
|
|
156
156
|
// sessionId would normalize off /chat, so drop too.
|
|
157
157
|
if (!target.sessionId || !isSafePathComponent(target.sessionId)) return undefined;
|
|
158
|
-
|
|
159
|
-
if (target.resultUuid) return `${sessionPath}?result=${encodeURIComponent(target.resultUuid)}`;
|
|
160
|
-
return sessionPath;
|
|
158
|
+
return `/${PAGE_ROUTES.chat}/${encodeURIComponent(target.sessionId)}`;
|
|
161
159
|
}
|
|
162
160
|
|
|
163
161
|
function buildFilesTarget(target: Extract<NavigateTarget, { view: typeof NOTIFICATION_VIEWS.files }>): string {
|
|
@@ -233,12 +231,12 @@ export function legacyActionToNavigateTarget(action: NotificationAction | undefi
|
|
|
233
231
|
* to the calling plugin so plugins cannot impersonate each other.
|
|
234
232
|
*
|
|
235
233
|
* This wrapper exists for host-side callers (`server/agent/`,
|
|
236
|
-
* `server/
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
234
|
+
* `server/workspace/`, `server/plugins/diagnostics.ts`) that don't
|
|
235
|
+
* have a `PluginRuntime` to hand. It forwards into `notifier.publish`
|
|
236
|
+
* with `lifecycle: "fyi"` and stashes legacy fields on `pluginData`
|
|
237
|
+
* so the bell can preserve icon / i18n / dedup semantics. macOS
|
|
238
|
+
* Reminder push is owned by the adapter subscribed to the notifier
|
|
239
|
+
* pubsub channel, not by this function.
|
|
242
240
|
*/
|
|
243
241
|
export function publishNotification(opts: PublishNotificationOpts): void {
|
|
244
242
|
const legacyId = opts.id ?? makeUuid();
|
|
@@ -250,7 +248,6 @@ export function publishNotification(opts: PublishNotificationOpts): void {
|
|
|
250
248
|
priority: opts.priority ?? NOTIFICATION_PRIORITIES.normal,
|
|
251
249
|
action,
|
|
252
250
|
i18n: opts.i18n,
|
|
253
|
-
transportId: opts.transportId,
|
|
254
251
|
sessionId: opts.sessionId,
|
|
255
252
|
};
|
|
256
253
|
// Fire-and-forget — the engine queues writes through its own
|
|
@@ -269,72 +266,3 @@ export function publishNotification(opts: PublishNotificationOpts): void {
|
|
|
269
266
|
log.warn("notifications", "publish failed", { error: String(err), legacyId, kind: opts.kind });
|
|
270
267
|
});
|
|
271
268
|
}
|
|
272
|
-
|
|
273
|
-
// ── Legacy test notification (kept for PoC endpoint) ────────────
|
|
274
|
-
|
|
275
|
-
export const DEFAULT_NOTIFICATION_MESSAGE = "Test notification";
|
|
276
|
-
export const DEFAULT_NOTIFICATION_TRANSPORT_ID = "cli";
|
|
277
|
-
|
|
278
|
-
export interface ScheduleNotificationOptions {
|
|
279
|
-
message?: string;
|
|
280
|
-
body?: string;
|
|
281
|
-
delaySeconds?: number;
|
|
282
|
-
transportId?: string;
|
|
283
|
-
// Optional deep-link action — lets dev-side callers fire a
|
|
284
|
-
// notification that navigates to a specific permalink when
|
|
285
|
-
// clicked. Without this the fired notification has no click
|
|
286
|
-
// behaviour (same as before #762).
|
|
287
|
-
action?: NotificationAction;
|
|
288
|
-
// Optional kind override — lets the manual-test helper fire a
|
|
289
|
-
// representative notification for every NotificationKind (todo,
|
|
290
|
-
// scheduler, agent, …) so the bell's icons can be eyeballed.
|
|
291
|
-
kind?: NotificationKind;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export interface ScheduledNotification {
|
|
295
|
-
firesAt: string;
|
|
296
|
-
delaySeconds: number;
|
|
297
|
-
cancel: () => void;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** PoC: schedules one `publishNotification` after a delay. Bridge
|
|
301
|
-
* push is no longer sent inline here — the bridge adapter (subscribed
|
|
302
|
-
* to the notifier channel) fans out to bridges based on the entry's
|
|
303
|
-
* `pluginData.transportId`. */
|
|
304
|
-
export function scheduleTestNotification(opts: ScheduleNotificationOptions): ScheduledNotification {
|
|
305
|
-
const message = opts.message ?? DEFAULT_NOTIFICATION_MESSAGE;
|
|
306
|
-
const transportId = opts.transportId ?? DEFAULT_NOTIFICATION_TRANSPORT_ID;
|
|
307
|
-
const delaySeconds = clampDelay(opts.delaySeconds);
|
|
308
|
-
const delayMs = delaySeconds * ONE_SECOND_MS;
|
|
309
|
-
const kind = opts.kind ?? NOTIFICATION_KINDS.push;
|
|
310
|
-
|
|
311
|
-
const firesAt = new Date(Date.now() + delayMs).toISOString();
|
|
312
|
-
|
|
313
|
-
const timer = setTimeout(() => {
|
|
314
|
-
publishNotification({
|
|
315
|
-
kind,
|
|
316
|
-
title: message,
|
|
317
|
-
body: opts.body,
|
|
318
|
-
priority: NOTIFICATION_PRIORITIES.normal,
|
|
319
|
-
action: opts.action,
|
|
320
|
-
transportId,
|
|
321
|
-
});
|
|
322
|
-
}, delayMs);
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
firesAt,
|
|
326
|
-
delaySeconds,
|
|
327
|
-
cancel: () => clearTimeout(timer),
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const DEFAULT_DELAY_SECONDS = 60;
|
|
332
|
-
|
|
333
|
-
function clampDelay(raw: number | undefined): number {
|
|
334
|
-
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
335
|
-
return DEFAULT_DELAY_SECONDS;
|
|
336
|
-
}
|
|
337
|
-
if (raw < 0) return 0;
|
|
338
|
-
if (raw > MAX_NOTIFICATION_DELAY_SEC) return MAX_NOTIFICATION_DELAY_SEC;
|
|
339
|
-
return Math.floor(raw);
|
|
340
|
-
}
|
package/server/index.ts
CHANGED
|
@@ -36,8 +36,9 @@ import { loadPresetPlugins } from "./plugins/preset-loader.js";
|
|
|
36
36
|
import { registerRuntimePlugins } from "./plugins/runtime-registry.js";
|
|
37
37
|
import { makePluginRuntime } from "./plugins/runtime.js";
|
|
38
38
|
import { MCP_PLUGIN_NAMES } from "./agent/plugin-names.js";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
39
|
+
import { setActiveBackend } from "./agent/backend/index.js";
|
|
40
|
+
import { fakeEchoBackend } from "./agent/backend/fake-echo.js";
|
|
41
|
+
import { startMacosReminderAdapter } from "./notifier/macosReminderAdapter.js";
|
|
41
42
|
import notifierRoutes from "./api/routes/notifier.js";
|
|
42
43
|
import { initNotifier } from "./notifier/engine.js";
|
|
43
44
|
import { registerSaveAttachmentHook } from "./utils/files/attachment-store.js";
|
|
@@ -45,6 +46,7 @@ import { capturePhotoLocation } from "./workspace/photo-locations/index.js";
|
|
|
45
46
|
import { createJournalRouter } from "./api/routes/journal.js";
|
|
46
47
|
import { createTranslationRouter } from "./api/routes/translation.js";
|
|
47
48
|
import { announcePluginMetaDiagnostics } from "./plugins/diagnostics.js";
|
|
49
|
+
import { announceOptionalDeps } from "./system/announceOptionalDeps.js";
|
|
48
50
|
import { createChatService } from "@mulmobridge/chat-service";
|
|
49
51
|
import { readSessionJsonl } from "./utils/files/session-io.js";
|
|
50
52
|
import { onSessionEvent, initSessionStore } from "./events/session-store/index.js";
|
|
@@ -56,6 +58,8 @@ import { WORKSPACE_PATHS } from "./workspace/paths.js";
|
|
|
56
58
|
import { serverError } from "./utils/httpError.js";
|
|
57
59
|
import { makeUuid } from "./utils/id.js";
|
|
58
60
|
import { mcpToolsRouter, mcpTools, isMcpToolEnabled } from "./agent/mcp-tools/index.js";
|
|
61
|
+
import { preflightUserServers, logPreflightResult } from "./agent/mcpPreflight.js";
|
|
62
|
+
import { loadMcpConfig } from "./system/config.js";
|
|
59
63
|
import { initWorkspace, workspacePath } from "./workspace/workspace.js";
|
|
60
64
|
import { runMemoryMigrationOnce } from "./workspace/memory/run.js";
|
|
61
65
|
import { runTopicMigrationOnce } from "./workspace/memory/topic-run.js";
|
|
@@ -91,7 +95,7 @@ import { EVENT_TYPES } from "../src/types/events.js";
|
|
|
91
95
|
import { SESSION_ORIGINS } from "../src/types/session.js";
|
|
92
96
|
import { buildHtmlPreviewCsp } from "../src/utils/html/previewCsp.js";
|
|
93
97
|
import { readAndInjectHtmlArtifact } from "./utils/html/htmlArtifactSplicer.js";
|
|
94
|
-
import { ONE_SECOND_MS, ONE_MINUTE_MS, ONE_HOUR_MS, STARTUP_FAILURE_FORCE_EXIT_MS } from "./utils/time.js";
|
|
98
|
+
import { ONE_SECOND_MS, ONE_MINUTE_MS, ONE_HOUR_MS, STARTUP_FAILURE_FORCE_EXIT_MS, FATAL_LOG_FLUSH_MS } from "./utils/time.js";
|
|
95
99
|
import { isPortFree, findAvailablePort, MAX_PORT_PROBES } from "./utils/port.mjs";
|
|
96
100
|
import { SCHEDULE_TYPES, MISSED_RUN_POLICIES } from "@receptron/task-scheduler";
|
|
97
101
|
|
|
@@ -102,6 +106,46 @@ const __dirname = path.dirname(__filename);
|
|
|
102
106
|
|
|
103
107
|
const debugMode = process.argv.includes("--debug");
|
|
104
108
|
|
|
109
|
+
// Global crash diagnostics (#1364). These handlers log loudly so a
|
|
110
|
+
// fatal failure is triagable, then EXIT — keeping the loop running
|
|
111
|
+
// after an uncaught exception is process-unsafe per the Node docs
|
|
112
|
+
// (invariants may already be broken). The launcher / supervisor
|
|
113
|
+
// (Electron wrapper, systemd, etc.) is responsible for restart.
|
|
114
|
+
//
|
|
115
|
+
// The canonical failure this PR set out to fix — missing `claude`
|
|
116
|
+
// on PATH crashing the server via spawn's `error` event — is now
|
|
117
|
+
// caught at the local boundary in `server/agent/backend/claude-code.ts`
|
|
118
|
+
// (an explicit `error` listener turns ENOENT into an AgentEvent).
|
|
119
|
+
// These handlers are the BACKSTOP for anything we missed, not a
|
|
120
|
+
// substitute for local error handling. (Codex review on #1364.)
|
|
121
|
+
//
|
|
122
|
+
// `process.exit(1)` is non-zero so supervisors that branch on exit
|
|
123
|
+
// code treat the bounce as an error condition.
|
|
124
|
+
process.on("uncaughtException", (err) => {
|
|
125
|
+
log.error("uncaughtException", err instanceof Error ? err.message : String(err), {
|
|
126
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
127
|
+
});
|
|
128
|
+
// Tiny grace so the log line flushes to disk before we exit.
|
|
129
|
+
setTimeout(() => process.exit(1), FATAL_LOG_FLUSH_MS);
|
|
130
|
+
});
|
|
131
|
+
process.on("unhandledRejection", (reason) => {
|
|
132
|
+
log.error("unhandledRejection", reason instanceof Error ? reason.message : String(reason), {
|
|
133
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
134
|
+
});
|
|
135
|
+
setTimeout(() => process.exit(1), FATAL_LOG_FLUSH_MS);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Test-seam: CI runs without a Claude CLI / API key set the
|
|
139
|
+
// MULMOCLAUDE_FAKE_AGENT env var, which swaps in an echo-stub
|
|
140
|
+
// backend so the chat flow still completes. Decided once at boot;
|
|
141
|
+
// the orchestrator reads the active backend with zero per-call
|
|
142
|
+
// overhead. Production callers never trip this branch (no runtime
|
|
143
|
+
// import-time cost beyond the small fake-echo module itself).
|
|
144
|
+
if (process.env.MULMOCLAUDE_FAKE_AGENT === "1") {
|
|
145
|
+
setActiveBackend(fakeEchoBackend);
|
|
146
|
+
log.info("agent", "MULMOCLAUDE_FAKE_AGENT=1 — active backend = fake-echo");
|
|
147
|
+
}
|
|
148
|
+
|
|
105
149
|
initWorkspace();
|
|
106
150
|
|
|
107
151
|
// Fire-and-forget memory migrations: legacy `memory.md` → atomic
|
|
@@ -653,7 +697,6 @@ app.use(chatService.router);
|
|
|
653
697
|
// `startRuntimeServices` has it. Calls that arrive before fill-in
|
|
654
698
|
// (impossible in practice — the HTTP server isn't listening yet)
|
|
655
699
|
// would no-op on publish but still queue the bridge push.
|
|
656
|
-
app.use(createNotificationsRouter());
|
|
657
700
|
app.use(notifierRoutes);
|
|
658
701
|
app.use(createJournalRouter());
|
|
659
702
|
app.use(createTranslationRouter());
|
|
@@ -768,6 +811,29 @@ function logMcpStatus(): void {
|
|
|
768
811
|
const names = disabledMcpTools.map((toolDef) => `${toolDef.definition.name} (${(toolDef.requiredEnv ?? []).join(", ")})`).join(", ");
|
|
769
812
|
log.info("mcp", "Unavailable (missing env)", { tools: names });
|
|
770
813
|
}
|
|
814
|
+
logExternalMcpPreflight();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// External MCP servers (the `mcp.json` ones — Notion / GitHub /…)
|
|
818
|
+
// get a separate preflight pass that mirrors the built-in
|
|
819
|
+
// `Available / Unavailable` summary above. Servers with catalog
|
|
820
|
+
// entries whose `required: true` fields are unset are excluded from
|
|
821
|
+
// the config handed to Claude Code (filtered inside
|
|
822
|
+
// `prepareUserServers`); this boot-time log gives the operator one
|
|
823
|
+
// clear startup signal (#1352).
|
|
824
|
+
function logExternalMcpPreflight(): void {
|
|
825
|
+
try {
|
|
826
|
+
const userMcpRaw = loadMcpConfig().mcpServers;
|
|
827
|
+
const preflight = preflightUserServers(userMcpRaw);
|
|
828
|
+
logPreflightResult(preflight, "boot");
|
|
829
|
+
} catch (err) {
|
|
830
|
+
// Best-effort: a broken mcp.json shouldn't take down boot. The
|
|
831
|
+
// per-agent-run path will still attempt the preflight and surface
|
|
832
|
+
// any genuine issue when the user actually starts a chat.
|
|
833
|
+
log.warn("mcp", "preflight at boot failed; will retry per-agent-run", {
|
|
834
|
+
error: err instanceof Error ? err.message : String(err),
|
|
835
|
+
});
|
|
836
|
+
}
|
|
771
837
|
}
|
|
772
838
|
|
|
773
839
|
function maybeForceJournalRun(): void {
|
|
@@ -807,11 +873,9 @@ async function startRuntimeServices(httpServer: ReturnType<typeof app.listen>, p
|
|
|
807
873
|
// is forwarded in here so the rest of `startRuntimeServices` can
|
|
808
874
|
// share the same instance.
|
|
809
875
|
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
// legacy `publishNotification()` did inline before PR 4.
|
|
814
|
-
startLegacyAdapters({ pushToBridge: chatService.pushToBridge });
|
|
876
|
+
// macOS Reminder adapter wiring lives in the `app.listen` callback,
|
|
877
|
+
// alongside `initNotifier`, so it's subscribed before the first
|
|
878
|
+
// await opens a publish-can-fire-but-no-one's-listening window.
|
|
815
879
|
|
|
816
880
|
// --- Plugin META aggregator diagnostics ---
|
|
817
881
|
// After the notifier engine is initialized so the wrapper has a
|
|
@@ -820,6 +884,12 @@ async function startRuntimeServices(httpServer: ReturnType<typeof app.listen>, p
|
|
|
820
884
|
// notification.
|
|
821
885
|
await announcePluginMetaDiagnostics();
|
|
822
886
|
|
|
887
|
+
// --- Optional host-dependency probe (#1385) ---
|
|
888
|
+
// Probes docker / ffmpeg / … once, warns (log + bell) for any
|
|
889
|
+
// missing one so a feature degrading is visible instead of a
|
|
890
|
+
// later opaque crash. Never throws.
|
|
891
|
+
await announceOptionalDeps();
|
|
892
|
+
|
|
823
893
|
// --- Chat socket transport (Phase A of #268) ---
|
|
824
894
|
chatService.attachSocket(httpServer);
|
|
825
895
|
|
|
@@ -1123,6 +1193,14 @@ process.on("SIGTERM", () => {
|
|
|
1123
1193
|
initNotifier({
|
|
1124
1194
|
publish: (channel, payload) => earlyPubsub.publish(channel, payload),
|
|
1125
1195
|
});
|
|
1196
|
+
// Subscribe the macOS Reminder side-channel BEFORE the first
|
|
1197
|
+
// await below — `initNotifier` opens the engine to publishes,
|
|
1198
|
+
// and any boot-time diagnostic that lands during the
|
|
1199
|
+
// `.server-port` write / `startRuntimeServices` setup would
|
|
1200
|
+
// otherwise miss the Reminder fan-out (CodeRabbit review on
|
|
1201
|
+
// PR #1358). The adapter is sync + no-op outside darwin, so
|
|
1202
|
+
// wiring it here costs nothing.
|
|
1203
|
+
startMacosReminderAdapter();
|
|
1126
1204
|
|
|
1127
1205
|
// Publish the actually-bound port so the hook script can
|
|
1128
1206
|
// address us — the requested PORT may have walked forward
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// macOS Reminder side-channel adapter for the notifier engine.
|
|
2
|
+
//
|
|
3
|
+
// Subscribes to the engine's `published` events and fires
|
|
4
|
+
// `pushToMacosReminder` for each one — that helper is itself a no-op
|
|
5
|
+
// outside darwin / when `DISABLE_MACOS_REMINDER_NOTIFICATIONS=1`, so
|
|
6
|
+
// the adapter is safe to start unconditionally.
|
|
7
|
+
//
|
|
8
|
+
// History: this file used to be `legacy-adapters.ts` and carried a
|
|
9
|
+
// second branch that fanned out to chat-service bridges based on a
|
|
10
|
+
// `transportId` field on `pluginData`. That branch was dead code —
|
|
11
|
+
// the only callers setting `transportId` were the PoC
|
|
12
|
+
// `/api/notifications/test` route and `scheduleTestNotification`,
|
|
13
|
+
// both removed in the same change. Real production publishers
|
|
14
|
+
// (mcp-tools/notify, sources/pipeline/notify, plugins/diagnostics,
|
|
15
|
+
// mcpFailureMonitor) never set the field, so no behaviour changed.
|
|
16
|
+
// If a future use case wants bridge fan-out it should arrive with a
|
|
17
|
+
// concrete caller and a designed API, not as latent scaffolding.
|
|
18
|
+
|
|
19
|
+
import { onEvent } from "./engine.js";
|
|
20
|
+
import { pushToMacosReminder } from "../system/macosNotify.js";
|
|
21
|
+
|
|
22
|
+
/** Wire the macOS Reminder sink as an in-process listener on the
|
|
23
|
+
* notifier engine. Returns an unsubscribe function for tests /
|
|
24
|
+
* teardown. */
|
|
25
|
+
export function startMacosReminderAdapter(): () => void {
|
|
26
|
+
return onEvent((event) => {
|
|
27
|
+
if (event.type !== "published") return;
|
|
28
|
+
void pushToMacosReminder(event.entry.title, event.entry.body);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Boot-time graceful-degradation announcement for missing optional
|
|
2
|
+
// host binaries (#1385). Probes the registry, then for each missing
|
|
3
|
+
// dependency emits one structured log.warn plus a deduped bell
|
|
4
|
+
// notification naming the affected feature/plugins. Never throws —
|
|
5
|
+
// degradation is the whole point.
|
|
6
|
+
|
|
7
|
+
import { BUILT_IN_PLUGIN_METAS } from "../../src/plugins/metas.js";
|
|
8
|
+
import type { PluginMeta } from "../../src/plugins/meta-types.js";
|
|
9
|
+
import { NOTIFICATION_PRIORITIES } from "../../src/types/notification.js";
|
|
10
|
+
import { log } from "./logger/index.js";
|
|
11
|
+
import { publishNotification } from "../events/notifications.js";
|
|
12
|
+
import { probeOptionalDeps, optionalDeps } from "./optionalDeps.js";
|
|
13
|
+
|
|
14
|
+
function pluginsRequiring(depId: string): string[] {
|
|
15
|
+
const metas: readonly PluginMeta[] = Object.values(BUILT_IN_PLUGIN_METAS);
|
|
16
|
+
return metas.filter((meta) => meta.requires?.includes(depId)).map((meta) => meta.toolName);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function announceOptionalDeps(): Promise<void> {
|
|
20
|
+
const statuses = await probeOptionalDeps();
|
|
21
|
+
for (const dep of optionalDeps()) {
|
|
22
|
+
const status = statuses[dep.id];
|
|
23
|
+
if (!status || status.available) continue;
|
|
24
|
+
const affectedPlugins = pluginsRequiring(dep.id);
|
|
25
|
+
// `not-on-path` → install it; `probe-failed` → it's installed
|
|
26
|
+
// but not responding (e.g. the docker daemon is down). The
|
|
27
|
+
// remediation differs, so the copy must be reason-aware rather
|
|
28
|
+
// than always saying "not found" (Codex review).
|
|
29
|
+
const notFound = status.reason === "not-on-path";
|
|
30
|
+
log.warn("deps", `optional dependency '${dep.command}' unavailable — ${dep.enables} degraded`, {
|
|
31
|
+
depId: dep.id,
|
|
32
|
+
reason: status.reason,
|
|
33
|
+
affectedPlugins,
|
|
34
|
+
});
|
|
35
|
+
publishNotification({
|
|
36
|
+
id: `optional-dep-missing:${dep.id}`,
|
|
37
|
+
kind: "system",
|
|
38
|
+
priority: NOTIFICATION_PRIORITIES.normal,
|
|
39
|
+
title: "Optional dependency unavailable",
|
|
40
|
+
body: notFound
|
|
41
|
+
? `${dep.command} not found — some features are disabled. Install it and restart.`
|
|
42
|
+
: `${dep.command} is installed but not responding — some features are disabled. Start it and restart.`,
|
|
43
|
+
i18n: {
|
|
44
|
+
titleKey: "optionalDeps.title",
|
|
45
|
+
bodyKey: notFound ? "optionalDeps.notFound" : "optionalDeps.notResponding",
|
|
46
|
+
bodyParams: { command: dep.command },
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
package/server/system/config.ts
CHANGED
|
@@ -292,10 +292,17 @@ export function isMcpServerSpec(value: unknown): value is McpServerSpec {
|
|
|
292
292
|
|
|
293
293
|
// Workspace id must be slug-shaped so it survives being used as the
|
|
294
294
|
// mcpServers map key and in the `mcp__<id>__<tool>` tool naming.
|
|
295
|
+
//
|
|
296
|
+
// Consecutive `__` is forbidden inside the id because `__` is the
|
|
297
|
+
// delimiter in the tool-name encoding — a server id like `foo__bar`
|
|
298
|
+
// produces `mcp__foo__bar__tool`, which is ambiguous between server
|
|
299
|
+
// `foo` (tool `bar__tool`) and server `foo__bar` (tool `tool`).
|
|
300
|
+
// Forbidding `__` in the id keeps the convention unambiguous
|
|
301
|
+
// everywhere (Codex review on #1356).
|
|
295
302
|
const MCP_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
296
303
|
|
|
297
304
|
export function isMcpServerId(value: unknown): value is string {
|
|
298
|
-
return typeof value === "string" && MCP_ID_RE.test(value);
|
|
305
|
+
return typeof value === "string" && MCP_ID_RE.test(value) && !value.includes("__");
|
|
299
306
|
}
|
|
300
307
|
|
|
301
308
|
export function isMcpConfigFile(value: unknown): value is McpConfigFile {
|
package/server/system/docker.ts
CHANGED
|
@@ -41,18 +41,26 @@ function assertClaudeFiles(): void {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
/** Pure daemon-liveness probe: `docker ps -q` succeeds only when the
|
|
45
|
+
* client is installed AND the daemon is reachable. No config or
|
|
46
|
+
* caching concerns — the optional-deps registry owns the PATH check
|
|
47
|
+
* and caching; this is just the liveness half. */
|
|
48
|
+
export async function isDockerLive(): Promise<boolean> {
|
|
48
49
|
try {
|
|
49
50
|
await execFileAsync("docker", ["ps", "-q"], {
|
|
50
51
|
timeout: SUBPROCESS_PROBE_TIMEOUT_MS,
|
|
51
52
|
});
|
|
52
|
-
|
|
53
|
+
return true;
|
|
53
54
|
} catch {
|
|
54
|
-
|
|
55
|
+
return false;
|
|
55
56
|
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function isDockerAvailable(): Promise<boolean> {
|
|
60
|
+
if (env.disableSandbox) return false;
|
|
61
|
+
if (_dockerEnabled !== null) return _dockerEnabled;
|
|
62
|
+
assertClaudeFiles();
|
|
63
|
+
_dockerEnabled = await isDockerLive();
|
|
56
64
|
return _dockerEnabled;
|
|
57
65
|
}
|
|
58
66
|
|
package/server/system/env.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
// `docs/developer.md` lists every env var and what it does; this
|
|
16
16
|
// module is the runtime side of that table.
|
|
17
17
|
|
|
18
|
+
import { CLI_FLAGS } from "../utils/cli-flags.mjs";
|
|
19
|
+
|
|
18
20
|
// ── Type coercion helpers ───────────────────────────────────────────
|
|
19
21
|
|
|
20
22
|
function asInt(value: string | undefined, fallback: number, opts: { min?: number; max?: number } = {}): number {
|
|
@@ -33,6 +35,17 @@ function asFlag(value: string | undefined): boolean {
|
|
|
33
35
|
return value === "1";
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
// Env vars also switched on by a CLI flag on this process's argv.
|
|
39
|
+
// The npx launcher injects the env var into the spawned server, so
|
|
40
|
+
// its path doesn't rely on this; this covers a direct
|
|
41
|
+
// `tsx server/index.ts` / `yarn dev --<flag>` run. Computed once at
|
|
42
|
+
// module load — same lifetime as the env snapshot below. (#1089.)
|
|
43
|
+
const argvEnabledEnv = new Set<string>(CLI_FLAGS.filter(({ flag }) => process.argv.includes(flag)).map(({ env: envName }) => envName));
|
|
44
|
+
|
|
45
|
+
function flagOf(envName: string): boolean {
|
|
46
|
+
return asFlag(process.env[envName]) || argvEnabledEnv.has(envName);
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
function asCsv(value: string | undefined): readonly string[] {
|
|
37
50
|
return Object.freeze(
|
|
38
51
|
(value ?? "")
|
|
@@ -57,13 +70,13 @@ export const env = Object.freeze({
|
|
|
57
70
|
isProduction: process.env.NODE_ENV === "production",
|
|
58
71
|
|
|
59
72
|
// Sandbox / Docker
|
|
60
|
-
disableSandbox:
|
|
73
|
+
disableSandbox: flagOf("DISABLE_SANDBOX"),
|
|
61
74
|
// Debug aid: also persist `tool_call` events to the session
|
|
62
75
|
// jsonl (the `tool_result` side already lands on disk). Off by
|
|
63
76
|
// default because args can be large and may carry payload bytes
|
|
64
77
|
// the user didn't expect to land in this exact form. See
|
|
65
78
|
// plans/done/feat-persist-tool-calls.md / issue #1096.
|
|
66
|
-
persistToolCalls:
|
|
79
|
+
persistToolCalls: flagOf("PERSIST_TOOL_CALLS"),
|
|
67
80
|
// Host-credential opt-ins for the Docker sandbox (#259). Both off
|
|
68
81
|
// by default. See docs/sandbox-credentials.md for the contract.
|
|
69
82
|
sandboxSshAgentForward: asFlag(process.env.SANDBOX_SSH_AGENT_FORWARD),
|
|
@@ -89,8 +102,8 @@ export const env = Object.freeze({
|
|
|
89
102
|
// Debug-only force-run flags. Off by default; `=1` triggers an
|
|
90
103
|
// immediate run on startup instead of waiting for the scheduled
|
|
91
104
|
// interval.
|
|
92
|
-
journalForceRunOnStartup:
|
|
93
|
-
chatIndexForceRunOnStartup:
|
|
105
|
+
journalForceRunOnStartup: flagOf("JOURNAL_FORCE_RUN_ON_STARTUP"),
|
|
106
|
+
chatIndexForceRunOnStartup: flagOf("CHAT_INDEX_FORCE_RUN_ON_STARTUP"),
|
|
94
107
|
|
|
95
108
|
// macOS Reminder notification sink (#789). Darwin-only; iCloud
|
|
96
109
|
// Reminders sync mirrors the entry to the user's iPhone, which
|
|
@@ -100,7 +113,7 @@ export const env = Object.freeze({
|
|
|
100
113
|
// `DISABLE_MACOS_REMINDER_NOTIFICATIONS=1` to opt out (e.g. for
|
|
101
114
|
// a shared dev box where the iPhone owner shouldn't get pinged).
|
|
102
115
|
// Mirrors the `DISABLE_SANDBOX` convention.
|
|
103
|
-
disableMacosReminderNotifications:
|
|
116
|
+
disableMacosReminderNotifications: flagOf("DISABLE_MACOS_REMINDER_NOTIFICATIONS"),
|
|
104
117
|
|
|
105
118
|
// MulmoBridge Relay (#520). Optional — when both are set the server
|
|
106
119
|
// connects to the Relay via WebSocket and forwards bridge messages.
|