mulmoclaude 0.6.2 → 0.6.4
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/JsonEditor-D6WBWLoa.js +10 -0
- package/client/assets/JsonEditor-Di5xGeZY.css +1 -0
- package/client/assets/_plugin-vue_export-helper-BOai-rQB.js +1 -0
- package/client/assets/chunk-D8eiyYIV-LcKZGJv5.js +1 -0
- package/client/assets/{html2canvas-CDGcmOD3-Bkf2uOth.js → html2canvas-CDGcmOD3-XVrO-eyz.js} +1 -1
- package/client/assets/index-CyBr8Mkr.css +2 -0
- package/client/assets/index-zZIqEbNX.js +5106 -0
- package/client/assets/{index.es-DqtpmBm8-D9mAh_KQ.js → index.es-DqtpmBm8-DHT6q10o.js} +1 -1
- package/client/assets/material-symbols-outlined-DtIK7AQn.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-D6kcV0wa.js +1 -0
- package/client/assets/{runtime-vue-BVUzgYGA.js → runtime-vue-fFYhnNg3.js} +1 -1
- package/client/assets/{vue-C8UuIO9J.js → vue-D4w8THF_.js} +1 -1
- package/client/assets/vue-i18n-CQbxVmNs.js +3 -0
- package/client/assets/vue.runtime.esm-bundler-BTyIdNAI.js +4 -0
- package/client/index.html +10 -10
- package/package.json +9 -8
- 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 +74 -24
- package/server/agent/index.ts +104 -80
- package/server/agent/mcpFailureMonitor.ts +167 -0
- package/server/agent/mcpPreflight.ts +185 -0
- package/server/agent/prompt.ts +50 -359
- package/server/agent/stdioHttpShim.ts +171 -0
- package/server/agent/stream.ts +12 -1
- package/server/api/routes/encore.ts +55 -0
- package/server/api/routes/files.ts +22 -0
- 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/build/dispatcher.mjs +299 -0
- package/server/encore/INVARIANTS.md +272 -0
- package/server/encore/boot.ts +39 -0
- package/server/encore/closure.ts +36 -0
- package/server/encore/cycle.ts +276 -0
- package/server/encore/dispatch.ts +103 -0
- package/server/encore/handlers/amend.ts +99 -0
- package/server/encore/handlers/appendNote.ts +74 -0
- package/server/encore/handlers/defineEncore.ts +42 -0
- package/server/encore/handlers/listTickets.ts +107 -0
- package/server/encore/handlers/markStepDone.ts +41 -0
- package/server/encore/handlers/markTargetSkipped.ts +33 -0
- package/server/encore/handlers/query.ts +138 -0
- package/server/encore/handlers/recordValues.ts +44 -0
- package/server/encore/handlers/resolveNotification.ts +121 -0
- package/server/encore/handlers/setup.ts +81 -0
- package/server/encore/handlers/shared.ts +137 -0
- package/server/encore/handlers/snooze.ts +87 -0
- package/server/encore/handlers/startObligationChat.ts +64 -0
- package/server/encore/handlers/startSetupChat.ts +50 -0
- package/server/encore/lock.ts +61 -0
- package/server/encore/notifier.ts +123 -0
- package/server/encore/obligation.ts +25 -0
- package/server/encore/paths.ts +78 -0
- package/server/encore/reconcile.ts +661 -0
- package/server/encore/tick.ts +191 -0
- package/server/encore/yaml-fm.ts +63 -0
- package/server/events/notifications.ts +19 -91
- package/server/index.ts +94 -9
- package/server/notifier/engine.ts +102 -1
- package/server/notifier/macosReminderAdapter.ts +30 -0
- package/server/notifier/runtime-api.ts +41 -1
- package/server/notifier/types.ts +15 -2
- package/server/plugins/runtime.ts +11 -2
- package/server/prompts/index.ts +39 -0
- package/server/prompts/system/journal-pointer.md +12 -0
- package/server/prompts/system/memory-management-atomic.md +33 -0
- package/server/prompts/system/memory-management-topic.md +60 -0
- package/server/prompts/system/news-concierge.md +24 -0
- package/server/prompts/system/sandbox-tools.md +10 -0
- package/server/prompts/system/sources-context.md +16 -0
- package/server/prompts/system/system.md +91 -0
- package/server/system/announceOptionalDeps.ts +57 -0
- package/server/system/appVersion.ts +34 -0
- package/server/system/config.ts +17 -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/files/encore-io.ts +111 -0
- package/server/utils/time.ts +6 -0
- package/server/workspace/helps/business.md +2 -2
- package/server/workspace/helps/encore-dsl.md +482 -0
- package/server/workspace/helps/index.md +15 -13
- package/server/workspace/helps/mulmoscript.md +3 -3
- package/server/workspace/helps/sandbox.md +2 -2
- package/server/workspace/hooks/dispatcher.ts +7 -5
- package/server/workspace/hooks/provision.ts +6 -3
- 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 +37 -8
- package/src/components/FileContentRenderer.vue +102 -9
- package/src/components/JsonEditor.vue +160 -0
- package/src/components/NotificationBell.vue +35 -3
- package/src/components/PluginLauncher.vue +20 -41
- package/src/components/RightSidebar.vue +19 -0
- package/src/components/SettingsMcpTab.vue +58 -11
- package/src/components/SettingsModal.vue +22 -1
- package/src/components/StackView.vue +10 -1
- package/src/components/TodoExplorer.vue +16 -0
- package/src/components/todo/TodoKanbanView.vue +34 -6
- package/src/composables/useNotifications.ts +21 -1
- package/src/config/apiRoutes.ts +0 -6
- package/src/config/mcpCatalog.ts +12 -7
- package/src/config/mcpTypes.ts +5 -0
- package/src/config/roles.ts +52 -15
- package/src/config/systemFileDescriptors.ts +12 -0
- package/src/lang/de.ts +108 -12
- package/src/lang/en.ts +105 -11
- package/src/lang/es.ts +106 -11
- package/src/lang/fr.ts +106 -11
- package/src/lang/ja.ts +104 -11
- package/src/lang/ko.ts +105 -11
- package/src/lang/pt-BR.ts +106 -11
- package/src/lang/zh.ts +103 -11
- package/src/main.ts +1 -0
- package/src/plugins/_generated/metas.ts +4 -0
- package/src/plugins/_generated/registrations.ts +2 -0
- package/src/plugins/_generated/server-bindings.ts +5 -0
- package/src/plugins/encore/EncoreDashboard.vue +504 -0
- package/src/plugins/encore/EncoreRedirect.vue +116 -0
- package/src/plugins/encore/View.vue +36 -0
- package/src/plugins/encore/defineEncoreDefinition.ts +74 -0
- package/src/plugins/encore/defineEncoreMeta.ts +13 -0
- package/src/plugins/encore/index.ts +93 -0
- package/src/plugins/encore/manageEncoreDefinition.ts +100 -0
- package/src/plugins/encore/manageEncoreMeta.ts +36 -0
- package/src/plugins/manageSkills/View.vue +832 -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/skill/View.vue +1 -5
- package/src/plugins/spreadsheet/View.vue +3 -3
- package/src/plugins/spreadsheet/definition.ts +1 -1
- package/src/plugins/textResponse/Preview.vue +14 -1
- package/src/plugins/textResponse/View.vue +39 -24
- package/src/plugins/wiki/components/WikiPageBody.vue +4 -0
- package/src/router/index.ts +11 -0
- package/src/router/pageRoutes.ts +1 -0
- package/src/types/encore-dsl/at-expression.ts +120 -0
- package/src/types/encore-dsl/at-resolver.ts +32 -0
- package/src/types/encore-dsl/cadence.ts +289 -0
- package/src/types/encore-dsl/schema.ts +288 -0
- package/src/types/notification.ts +2 -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/image/htmlSrcAttrs.ts +117 -13
- 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/server/workspace/hooks/dispatcher.mjs +0 -300
- package/src/composables/useSelectedResult.ts +0 -49
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Encore tick — time-driven invoker for the reconciler.
|
|
2
|
+
//
|
|
3
|
+
// The tick used to carry its own copy of trim/escalate/publish logic.
|
|
4
|
+
// That moved into `reconcile.ts` (the sole owner of bell state). The
|
|
5
|
+
// tick is now a thin walker:
|
|
6
|
+
//
|
|
7
|
+
// 1. For every obligation directory, reconcile the latest cycle.
|
|
8
|
+
// 2. Sweep stuck tickets — reconcile any (obligationId, cycleId)
|
|
9
|
+
// pair found in tickets/ that step 1 didn't cover. This
|
|
10
|
+
// retries clear failures on closed/non-latest cycles, which
|
|
11
|
+
// step 1 alone would skip (it only picks the latest cycle).
|
|
12
|
+
// 3. Prune orphan tickets older than 30 days.
|
|
13
|
+
//
|
|
14
|
+
// `Ticket` still lives here because dispatch.ts, reconcile.ts, and
|
|
15
|
+
// the host UI all import the type — keeping it adjacent to the on-disk
|
|
16
|
+
// shape (the only consumer is `tickets/*.json`) makes the locus of
|
|
17
|
+
// schema changes obvious.
|
|
18
|
+
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
|
|
21
|
+
import { log as defaultLog } from "../system/logger/index.js";
|
|
22
|
+
import { ONE_DAY_MS } from "../utils/time.js";
|
|
23
|
+
import type { Severity } from "../../src/types/encore-dsl/schema.js";
|
|
24
|
+
import { TICKETS_DIRNAME, OBLIGATIONS_DIRNAME } from "./paths.js";
|
|
25
|
+
import { readDir, readDirSubdirs, readTextOrNull, unlink } from "../utils/files/encore-io.js";
|
|
26
|
+
import * as encoreNotifier from "./notifier.js";
|
|
27
|
+
import { reconcileCycleNotifications } from "./reconcile.js";
|
|
28
|
+
|
|
29
|
+
const ORPHAN_TICKET_AGE_MS = 30 * ONE_DAY_MS;
|
|
30
|
+
|
|
31
|
+
export interface TickDeps {
|
|
32
|
+
now: Date;
|
|
33
|
+
log?: typeof defaultLog;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Shape of a ticket on disk. Authoritative record of every live
|
|
37
|
+
* Encore bell entry: which obligation+cycle+step it belongs to,
|
|
38
|
+
* which targets it covers, what severity it was published at
|
|
39
|
+
* (used for escalation diff), and the seed prompt
|
|
40
|
+
* resolveNotification will use to start the chat on user click.
|
|
41
|
+
*
|
|
42
|
+
* A ticket's existence asserts that a matching bell is alive and
|
|
43
|
+
* awaiting a `clear` operation. Tickets are regeneratable cache
|
|
44
|
+
* for everything except `chatSessionId` (the binding to a user's
|
|
45
|
+
* open chat); a sweep of `tickets/*.json` plus the matching bell
|
|
46
|
+
* entries leaves the system in a recoverable state — the
|
|
47
|
+
* reconciler rebuilds from cycle files on the next tick. */
|
|
48
|
+
export interface Ticket {
|
|
49
|
+
pendingId: string;
|
|
50
|
+
obligationId: string;
|
|
51
|
+
cycleId: string;
|
|
52
|
+
notificationId: string;
|
|
53
|
+
stepId: string;
|
|
54
|
+
/** Target ids covered by this bundled notification. */
|
|
55
|
+
targets: string[];
|
|
56
|
+
/** Severity at last publish — used as the escalation-diff
|
|
57
|
+
* baseline. The cycle file used to carry
|
|
58
|
+
* `lastPublishedSeverity`; that moved here when status flags
|
|
59
|
+
* were removed. */
|
|
60
|
+
severity: Severity;
|
|
61
|
+
/** Title and body as last rendered to the bell — the drift
|
|
62
|
+
* baseline. Trim path compares these against `bundleTitle` /
|
|
63
|
+
* `bundleBody` recomputed from the current DSL; if either has
|
|
64
|
+
* drifted (most commonly via `amendDefinition` editing a
|
|
65
|
+
* `displayName`), the path calls `notifier.update` in place
|
|
66
|
+
* instead of clear-then-publish. Optional on the wire so
|
|
67
|
+
* tickets written by pre-update-API builds load cleanly — they
|
|
68
|
+
* are treated as "always drifted" on first sight, which causes
|
|
69
|
+
* an idempotent in-place update to the bell and a ticket
|
|
70
|
+
* rewrite that backfills the fields. */
|
|
71
|
+
title?: string;
|
|
72
|
+
body?: string;
|
|
73
|
+
seedPrompt: string;
|
|
74
|
+
createdAt: string;
|
|
75
|
+
/** Filled by resolveNotification on first bell click. Subsequent
|
|
76
|
+
* clicks reuse it (idempotent). */
|
|
77
|
+
chatSessionId?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function runTick(deps: TickDeps): Promise<void> {
|
|
81
|
+
const log = deps.log ?? defaultLog;
|
|
82
|
+
const obligationIds = await readDirSubdirs(OBLIGATIONS_DIRNAME);
|
|
83
|
+
for (const obligationId of obligationIds) {
|
|
84
|
+
try {
|
|
85
|
+
await reconcileCycleNotifications({ obligationId, now: deps.now, log });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
log.warn("encore", "tick: reconcile failed", { obligationId, error: err instanceof Error ? err.message : String(err) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
await sweepStuckTickets(new Set(obligationIds), deps.now, log);
|
|
91
|
+
await pruneOrphanTickets(deps.now, log);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── stuck-ticket sweep (Phase 2) ──────────────────────────────────
|
|
95
|
+
//
|
|
96
|
+
// The per-obligation reconcile above only touches the obligation's
|
|
97
|
+
// latest cycle (and its just-provisioned successor on cycle-close).
|
|
98
|
+
// A ticket on a NON-latest cycle never gets revisited there — which
|
|
99
|
+
// matters because `safeClearBell` failures are intentionally
|
|
100
|
+
// non-destructive: when the clear fails, the reconciler keeps the
|
|
101
|
+
// ticket so a later attempt can retry. Without this sweep, that
|
|
102
|
+
// "later attempt" would never come for tickets on closed cycles
|
|
103
|
+
// (until the 30-day age-based prune).
|
|
104
|
+
//
|
|
105
|
+
// This sweep collects every (obligationId, cycleId) pair present in
|
|
106
|
+
// tickets/ and reconciles each. It overlaps Phase 1 for the
|
|
107
|
+
// latest cycle, but reconcile is idempotent (proven by the
|
|
108
|
+
// "idempotency" test in test_encore_reconcile.ts) so the redundancy
|
|
109
|
+
// is cheap and the contract stays simple.
|
|
110
|
+
|
|
111
|
+
async function sweepStuckTickets(knownObligationIds: Set<string>, now: Date, log: typeof defaultLog): Promise<void> {
|
|
112
|
+
const entries = await readDir(TICKETS_DIRNAME);
|
|
113
|
+
const pairsToReconcile = await collectStuckCyclePairs(entries, knownObligationIds);
|
|
114
|
+
for (const pair of pairsToReconcile) {
|
|
115
|
+
try {
|
|
116
|
+
await reconcileCycleNotifications({ obligationId: pair.obligationId, cycleId: pair.cycleId, now, log });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
log.warn("encore", "tick: stuck-ticket sweep failed", {
|
|
119
|
+
obligationId: pair.obligationId,
|
|
120
|
+
cycleId: pair.cycleId,
|
|
121
|
+
error: err instanceof Error ? err.message : String(err),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function collectStuckCyclePairs(entries: string[], knownObligationIds: Set<string>): Promise<{ obligationId: string; cycleId: string }[]> {
|
|
128
|
+
const seen = new Set<string>();
|
|
129
|
+
const out: { obligationId: string; cycleId: string }[] = [];
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (!entry.endsWith(".json")) continue;
|
|
132
|
+
const raw = await readTextOrNull(path.join(TICKETS_DIRNAME, entry));
|
|
133
|
+
if (!raw) continue;
|
|
134
|
+
let ticket: Ticket;
|
|
135
|
+
try {
|
|
136
|
+
ticket = JSON.parse(raw) as Ticket;
|
|
137
|
+
} catch {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Skip tickets pointing at obligations that no longer exist on
|
|
141
|
+
// disk — those get cleaned up by the age-based prune below.
|
|
142
|
+
if (!knownObligationIds.has(ticket.obligationId)) continue;
|
|
143
|
+
const key = `${ticket.obligationId}::${ticket.cycleId}`;
|
|
144
|
+
if (seen.has(key)) continue;
|
|
145
|
+
seen.add(key);
|
|
146
|
+
out.push({ obligationId: ticket.obligationId, cycleId: ticket.cycleId });
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── orphan ticket sweep (time-driven, lives here not in reconcile) ──
|
|
152
|
+
//
|
|
153
|
+
// Orphan tickets are records older than 30 days that somehow weren't
|
|
154
|
+
// trimmed by reconcile (e.g. the cycle file was deleted manually,
|
|
155
|
+
// or a host crash left a ticket without its bell counterpart). The
|
|
156
|
+
// sweep is age-based, not state-based, so it belongs with the
|
|
157
|
+
// time-driven tick rather than the state-driven reconciler.
|
|
158
|
+
|
|
159
|
+
async function pruneOneTicket(rel: string, raw: string, now: Date, log: typeof defaultLog): Promise<void> {
|
|
160
|
+
let ticket: Ticket;
|
|
161
|
+
try {
|
|
162
|
+
ticket = JSON.parse(raw) as Ticket;
|
|
163
|
+
} catch {
|
|
164
|
+
await unlink(rel);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const ageMs = now.getTime() - new Date(ticket.createdAt).getTime();
|
|
168
|
+
if (ageMs <= ORPHAN_TICKET_AGE_MS) return;
|
|
169
|
+
log.info("encore", "tick: pruning orphan ticket", { pendingId: ticket.pendingId, ageMs });
|
|
170
|
+
// Clear the host bell entry BEFORE unlinking the ticket. Otherwise
|
|
171
|
+
// the bell entry stays visible but the ticket is gone — next tick
|
|
172
|
+
// treats the step as un-fired and publishes a duplicate while the
|
|
173
|
+
// stale entry is still up.
|
|
174
|
+
try {
|
|
175
|
+
await encoreNotifier.clear(ticket.notificationId);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
log.warn("encore", "tick: prune-bell-clear failed", { notificationId: ticket.notificationId, error: err instanceof Error ? err.message : String(err) });
|
|
178
|
+
}
|
|
179
|
+
await unlink(rel);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function pruneOrphanTickets(now: Date, log: typeof defaultLog): Promise<void> {
|
|
183
|
+
const entries = await readDir(TICKETS_DIRNAME);
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (!entry.endsWith(".json")) continue;
|
|
186
|
+
const rel = path.join(TICKETS_DIRNAME, entry);
|
|
187
|
+
const raw = await readTextOrNull(rel);
|
|
188
|
+
if (!raw) continue;
|
|
189
|
+
await pruneOneTicket(rel, raw, now, log);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Encore-local YAML-frontmatter parser/serializer.
|
|
2
|
+
//
|
|
3
|
+
// The host's `server/utils/markdown/frontmatter.ts` uses
|
|
4
|
+
// `yaml.FAILSAFE_SCHEMA` on load, which keeps every scalar as a
|
|
5
|
+
// string. That's the right call for wiki/topic files where
|
|
6
|
+
// `'1.20'` / `'true'` need to round-trip verbatim, but it breaks
|
|
7
|
+
// Encore's DSL which expects real numbers (`version: 1`) and
|
|
8
|
+
// booleans (`required: true`) for Zod validation on re-parse.
|
|
9
|
+
//
|
|
10
|
+
// We use js-yaml's default schema (CORE_SCHEMA) on both load and
|
|
11
|
+
// dump so numbers + booleans + nulls round-trip natively. Output
|
|
12
|
+
// is byte-compatible with the host's serializer for the same
|
|
13
|
+
// `---\n...\n---\n\nbody` envelope shape.
|
|
14
|
+
|
|
15
|
+
import yaml from "js-yaml";
|
|
16
|
+
|
|
17
|
+
const FRONTMATTER_OPEN = /^---\r?\n/;
|
|
18
|
+
const FRONTMATTER_CLOSE = /(?:^|\r?\n)---\s*(?:\r?\n|$)/;
|
|
19
|
+
|
|
20
|
+
export interface ParsedMarkdown {
|
|
21
|
+
meta: Record<string, unknown>;
|
|
22
|
+
body: string;
|
|
23
|
+
hasHeader: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeLoad(text: string): Record<string, unknown> | null {
|
|
27
|
+
try {
|
|
28
|
+
const loaded = yaml.load(text);
|
|
29
|
+
if (loaded === null || loaded === undefined) return null;
|
|
30
|
+
if (typeof loaded !== "object" || Array.isArray(loaded)) return null;
|
|
31
|
+
return loaded as Record<string, unknown>;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parse a markdown document, splitting frontmatter from body. */
|
|
38
|
+
export function parseEncoreFrontmatter(raw: string): ParsedMarkdown {
|
|
39
|
+
if (!FRONTMATTER_OPEN.test(raw)) {
|
|
40
|
+
return { meta: {}, body: raw, hasHeader: false };
|
|
41
|
+
}
|
|
42
|
+
const afterOpen = raw.replace(FRONTMATTER_OPEN, "");
|
|
43
|
+
const closeMatch = FRONTMATTER_CLOSE.exec(afterOpen);
|
|
44
|
+
if (!closeMatch || closeMatch.index === undefined) {
|
|
45
|
+
return { meta: {}, body: raw, hasHeader: false };
|
|
46
|
+
}
|
|
47
|
+
const yamlText = afterOpen.slice(0, closeMatch.index);
|
|
48
|
+
const body = afterOpen.slice(closeMatch.index + closeMatch[0].length);
|
|
49
|
+
const meta = safeLoad(yamlText);
|
|
50
|
+
if (meta === null) {
|
|
51
|
+
return { meta: {}, body: raw, hasHeader: false };
|
|
52
|
+
}
|
|
53
|
+
return { meta, body, hasHeader: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Serialize a meta object + body back into the canonical
|
|
57
|
+
* `---\n...\n---\n\nbody` shape. Empty `meta` returns the body
|
|
58
|
+
* alone. */
|
|
59
|
+
export function serializeEncoreFrontmatter(meta: Record<string, unknown>, body: string): string {
|
|
60
|
+
if (Object.keys(meta).length === 0) return body;
|
|
61
|
+
const yamlText = yaml.dump(meta, { lineWidth: -1, noRefs: true }).trimEnd();
|
|
62
|
+
return `---\n${yamlText}\n---\n\n${body}`;
|
|
63
|
+
}
|
|
@@ -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
|
@@ -4,6 +4,7 @@ import path from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import agentRoutes, { startChat } from "./api/routes/agent.js";
|
|
6
6
|
import accountingRoutes from "./api/routes/accounting.js";
|
|
7
|
+
import encoreRoutes from "./api/routes/encore.js";
|
|
7
8
|
import photoLocationsRoutes from "./api/routes/photo-locations.js";
|
|
8
9
|
import schedulerRoutes from "./api/routes/scheduler.js";
|
|
9
10
|
import sessionsRoutes, { loadAllSessions } from "./api/routes/sessions.js";
|
|
@@ -36,8 +37,9 @@ import { loadPresetPlugins } from "./plugins/preset-loader.js";
|
|
|
36
37
|
import { registerRuntimePlugins } from "./plugins/runtime-registry.js";
|
|
37
38
|
import { makePluginRuntime } from "./plugins/runtime.js";
|
|
38
39
|
import { MCP_PLUGIN_NAMES } from "./agent/plugin-names.js";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
40
|
+
import { setActiveBackend } from "./agent/backend/index.js";
|
|
41
|
+
import { fakeEchoBackend } from "./agent/backend/fake-echo.js";
|
|
42
|
+
import { startMacosReminderAdapter } from "./notifier/macosReminderAdapter.js";
|
|
41
43
|
import notifierRoutes from "./api/routes/notifier.js";
|
|
42
44
|
import { initNotifier } from "./notifier/engine.js";
|
|
43
45
|
import { registerSaveAttachmentHook } from "./utils/files/attachment-store.js";
|
|
@@ -45,6 +47,8 @@ import { capturePhotoLocation } from "./workspace/photo-locations/index.js";
|
|
|
45
47
|
import { createJournalRouter } from "./api/routes/journal.js";
|
|
46
48
|
import { createTranslationRouter } from "./api/routes/translation.js";
|
|
47
49
|
import { announcePluginMetaDiagnostics } from "./plugins/diagnostics.js";
|
|
50
|
+
import { announceOptionalDeps } from "./system/announceOptionalDeps.js";
|
|
51
|
+
import { APP_VERSION } from "./system/appVersion.js";
|
|
48
52
|
import { createChatService } from "@mulmobridge/chat-service";
|
|
49
53
|
import { readSessionJsonl } from "./utils/files/session-io.js";
|
|
50
54
|
import { onSessionEvent, initSessionStore } from "./events/session-store/index.js";
|
|
@@ -56,6 +60,8 @@ import { WORKSPACE_PATHS } from "./workspace/paths.js";
|
|
|
56
60
|
import { serverError } from "./utils/httpError.js";
|
|
57
61
|
import { makeUuid } from "./utils/id.js";
|
|
58
62
|
import { mcpToolsRouter, mcpTools, isMcpToolEnabled } from "./agent/mcp-tools/index.js";
|
|
63
|
+
import { preflightUserServers, logPreflightResult } from "./agent/mcpPreflight.js";
|
|
64
|
+
import { loadMcpConfig } from "./system/config.js";
|
|
59
65
|
import { initWorkspace, workspacePath } from "./workspace/workspace.js";
|
|
60
66
|
import { runMemoryMigrationOnce } from "./workspace/memory/run.js";
|
|
61
67
|
import { runTopicMigrationOnce } from "./workspace/memory/topic-run.js";
|
|
@@ -86,12 +92,13 @@ import { logBackgroundError } from "./utils/logBackgroundError.js";
|
|
|
86
92
|
import { errorMessage } from "./utils/errors.js";
|
|
87
93
|
import { registerScheduledSkills } from "./workspace/skills/scheduler.js";
|
|
88
94
|
import { registerUserTasks } from "./workspace/skills/user-tasks.js";
|
|
95
|
+
import { registerEncoreTick } from "./encore/boot.js";
|
|
89
96
|
import { API_ROUTES } from "../src/config/apiRoutes.js";
|
|
90
97
|
import { EVENT_TYPES } from "../src/types/events.js";
|
|
91
98
|
import { SESSION_ORIGINS } from "../src/types/session.js";
|
|
92
99
|
import { buildHtmlPreviewCsp } from "../src/utils/html/previewCsp.js";
|
|
93
100
|
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";
|
|
101
|
+
import { ONE_SECOND_MS, ONE_MINUTE_MS, ONE_HOUR_MS, STARTUP_FAILURE_FORCE_EXIT_MS, FATAL_LOG_FLUSH_MS } from "./utils/time.js";
|
|
95
102
|
import { isPortFree, findAvailablePort, MAX_PORT_PROBES } from "./utils/port.mjs";
|
|
96
103
|
import { SCHEDULE_TYPES, MISSED_RUN_POLICIES } from "@receptron/task-scheduler";
|
|
97
104
|
|
|
@@ -102,6 +109,46 @@ const __dirname = path.dirname(__filename);
|
|
|
102
109
|
|
|
103
110
|
const debugMode = process.argv.includes("--debug");
|
|
104
111
|
|
|
112
|
+
// Global crash diagnostics (#1364). These handlers log loudly so a
|
|
113
|
+
// fatal failure is triagable, then EXIT — keeping the loop running
|
|
114
|
+
// after an uncaught exception is process-unsafe per the Node docs
|
|
115
|
+
// (invariants may already be broken). The launcher / supervisor
|
|
116
|
+
// (Electron wrapper, systemd, etc.) is responsible for restart.
|
|
117
|
+
//
|
|
118
|
+
// The canonical failure this PR set out to fix — missing `claude`
|
|
119
|
+
// on PATH crashing the server via spawn's `error` event — is now
|
|
120
|
+
// caught at the local boundary in `server/agent/backend/claude-code.ts`
|
|
121
|
+
// (an explicit `error` listener turns ENOENT into an AgentEvent).
|
|
122
|
+
// These handlers are the BACKSTOP for anything we missed, not a
|
|
123
|
+
// substitute for local error handling. (Codex review on #1364.)
|
|
124
|
+
//
|
|
125
|
+
// `process.exit(1)` is non-zero so supervisors that branch on exit
|
|
126
|
+
// code treat the bounce as an error condition.
|
|
127
|
+
process.on("uncaughtException", (err) => {
|
|
128
|
+
log.error("uncaughtException", err instanceof Error ? err.message : String(err), {
|
|
129
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
130
|
+
});
|
|
131
|
+
// Tiny grace so the log line flushes to disk before we exit.
|
|
132
|
+
setTimeout(() => process.exit(1), FATAL_LOG_FLUSH_MS);
|
|
133
|
+
});
|
|
134
|
+
process.on("unhandledRejection", (reason) => {
|
|
135
|
+
log.error("unhandledRejection", reason instanceof Error ? reason.message : String(reason), {
|
|
136
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
137
|
+
});
|
|
138
|
+
setTimeout(() => process.exit(1), FATAL_LOG_FLUSH_MS);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Test-seam: CI runs without a Claude CLI / API key set the
|
|
142
|
+
// MULMOCLAUDE_FAKE_AGENT env var, which swaps in an echo-stub
|
|
143
|
+
// backend so the chat flow still completes. Decided once at boot;
|
|
144
|
+
// the orchestrator reads the active backend with zero per-call
|
|
145
|
+
// overhead. Production callers never trip this branch (no runtime
|
|
146
|
+
// import-time cost beyond the small fake-echo module itself).
|
|
147
|
+
if (process.env.MULMOCLAUDE_FAKE_AGENT === "1") {
|
|
148
|
+
setActiveBackend(fakeEchoBackend);
|
|
149
|
+
log.info("agent", "MULMOCLAUDE_FAKE_AGENT=1 — active backend = fake-echo");
|
|
150
|
+
}
|
|
151
|
+
|
|
105
152
|
initWorkspace();
|
|
106
153
|
|
|
107
154
|
// Fire-and-forget memory migrations: legacy `memory.md` → atomic
|
|
@@ -522,6 +569,7 @@ app.get(API_ROUTES.health, (_req: Request, res: Response) => {
|
|
|
522
569
|
const cores = cpus().length;
|
|
523
570
|
res.json({
|
|
524
571
|
status: "OK",
|
|
572
|
+
version: APP_VERSION,
|
|
525
573
|
geminiAvailable: isGeminiAvailable(),
|
|
526
574
|
sandboxEnabled,
|
|
527
575
|
cpu: { load1, cores },
|
|
@@ -549,6 +597,7 @@ app.get(API_ROUTES.sandbox, (_req: Request, res: Response) => {
|
|
|
549
597
|
// the `/api` literal into each `router.post(API_ROUTES.…)` call.
|
|
550
598
|
app.use(agentRoutes);
|
|
551
599
|
app.use(accountingRoutes);
|
|
600
|
+
app.use(encoreRoutes);
|
|
552
601
|
app.use(photoLocationsRoutes);
|
|
553
602
|
// todosRoutes removed (#1145) — todo is now a runtime plugin
|
|
554
603
|
// (`@mulmoclaude/todo-plugin`); the dispatch route is generated by
|
|
@@ -653,7 +702,6 @@ app.use(chatService.router);
|
|
|
653
702
|
// `startRuntimeServices` has it. Calls that arrive before fill-in
|
|
654
703
|
// (impossible in practice — the HTTP server isn't listening yet)
|
|
655
704
|
// would no-op on publish but still queue the bridge push.
|
|
656
|
-
app.use(createNotificationsRouter());
|
|
657
705
|
app.use(notifierRoutes);
|
|
658
706
|
app.use(createJournalRouter());
|
|
659
707
|
app.use(createTranslationRouter());
|
|
@@ -768,6 +816,29 @@ function logMcpStatus(): void {
|
|
|
768
816
|
const names = disabledMcpTools.map((toolDef) => `${toolDef.definition.name} (${(toolDef.requiredEnv ?? []).join(", ")})`).join(", ");
|
|
769
817
|
log.info("mcp", "Unavailable (missing env)", { tools: names });
|
|
770
818
|
}
|
|
819
|
+
logExternalMcpPreflight();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// External MCP servers (the `mcp.json` ones — Notion / GitHub /…)
|
|
823
|
+
// get a separate preflight pass that mirrors the built-in
|
|
824
|
+
// `Available / Unavailable` summary above. Servers with catalog
|
|
825
|
+
// entries whose `required: true` fields are unset are excluded from
|
|
826
|
+
// the config handed to Claude Code (filtered inside
|
|
827
|
+
// `prepareUserServers`); this boot-time log gives the operator one
|
|
828
|
+
// clear startup signal (#1352).
|
|
829
|
+
function logExternalMcpPreflight(): void {
|
|
830
|
+
try {
|
|
831
|
+
const userMcpRaw = loadMcpConfig().mcpServers;
|
|
832
|
+
const preflight = preflightUserServers(userMcpRaw);
|
|
833
|
+
logPreflightResult(preflight, "boot");
|
|
834
|
+
} catch (err) {
|
|
835
|
+
// Best-effort: a broken mcp.json shouldn't take down boot. The
|
|
836
|
+
// per-agent-run path will still attempt the preflight and surface
|
|
837
|
+
// any genuine issue when the user actually starts a chat.
|
|
838
|
+
log.warn("mcp", "preflight at boot failed; will retry per-agent-run", {
|
|
839
|
+
error: err instanceof Error ? err.message : String(err),
|
|
840
|
+
});
|
|
841
|
+
}
|
|
771
842
|
}
|
|
772
843
|
|
|
773
844
|
function maybeForceJournalRun(): void {
|
|
@@ -807,11 +878,9 @@ async function startRuntimeServices(httpServer: ReturnType<typeof app.listen>, p
|
|
|
807
878
|
// is forwarded in here so the rest of `startRuntimeServices` can
|
|
808
879
|
// share the same instance.
|
|
809
880
|
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
// legacy `publishNotification()` did inline before PR 4.
|
|
814
|
-
startLegacyAdapters({ pushToBridge: chatService.pushToBridge });
|
|
881
|
+
// macOS Reminder adapter wiring lives in the `app.listen` callback,
|
|
882
|
+
// alongside `initNotifier`, so it's subscribed before the first
|
|
883
|
+
// await opens a publish-can-fire-but-no-one's-listening window.
|
|
815
884
|
|
|
816
885
|
// --- Plugin META aggregator diagnostics ---
|
|
817
886
|
// After the notifier engine is initialized so the wrapper has a
|
|
@@ -820,6 +889,12 @@ async function startRuntimeServices(httpServer: ReturnType<typeof app.listen>, p
|
|
|
820
889
|
// notification.
|
|
821
890
|
await announcePluginMetaDiagnostics();
|
|
822
891
|
|
|
892
|
+
// --- Optional host-dependency probe (#1385) ---
|
|
893
|
+
// Probes docker / ffmpeg / … once, warns (log + bell) for any
|
|
894
|
+
// missing one so a feature degrading is visible instead of a
|
|
895
|
+
// later opaque crash. Never throws.
|
|
896
|
+
await announceOptionalDeps();
|
|
897
|
+
|
|
823
898
|
// --- Chat socket transport (Phase A of #268) ---
|
|
824
899
|
chatService.attachSocket(httpServer);
|
|
825
900
|
|
|
@@ -1027,6 +1102,8 @@ async function startRuntimeServices(httpServer: ReturnType<typeof app.listen>, p
|
|
|
1027
1102
|
})
|
|
1028
1103
|
.catch(logBackgroundError("user-tasks", "failed to register user tasks"));
|
|
1029
1104
|
|
|
1105
|
+
registerEncoreTick(taskManager);
|
|
1106
|
+
|
|
1030
1107
|
taskManager.start();
|
|
1031
1108
|
|
|
1032
1109
|
maybeForceJournalRun();
|
|
@@ -1123,6 +1200,14 @@ process.on("SIGTERM", () => {
|
|
|
1123
1200
|
initNotifier({
|
|
1124
1201
|
publish: (channel, payload) => earlyPubsub.publish(channel, payload),
|
|
1125
1202
|
});
|
|
1203
|
+
// Subscribe the macOS Reminder side-channel BEFORE the first
|
|
1204
|
+
// await below — `initNotifier` opens the engine to publishes,
|
|
1205
|
+
// and any boot-time diagnostic that lands during the
|
|
1206
|
+
// `.server-port` write / `startRuntimeServices` setup would
|
|
1207
|
+
// otherwise miss the Reminder fan-out (CodeRabbit review on
|
|
1208
|
+
// PR #1358). The adapter is sync + no-op outside darwin, so
|
|
1209
|
+
// wiring it here costs nothing.
|
|
1210
|
+
startMacosReminderAdapter();
|
|
1126
1211
|
|
|
1127
1212
|
// Publish the actually-bound port so the hook script can
|
|
1128
1213
|
// address us — the requested PORT may have walked forward
|