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,34 @@
|
|
|
1
|
+
// The running app's version, read once from the nearest package.json.
|
|
2
|
+
//
|
|
3
|
+
// Layout is the same shape in both modes, so `../../package.json`
|
|
4
|
+
// relative to this file resolves correctly without a workspace walk:
|
|
5
|
+
// - dev (`yarn dev`): <repo>/server/system/appVersion.ts → <repo>/package.json
|
|
6
|
+
// - tarball (`npx`): <pkgDir>/server/system/appVersion.ts → <pkgDir>/package.json
|
|
7
|
+
// The launcher keeps `packages/mulmoclaude/package.json` in lockstep
|
|
8
|
+
// with the root version at publish time, so either resolution yields
|
|
9
|
+
// the same user-facing app version.
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
import { errorMessage } from "../utils/errors.js";
|
|
16
|
+
import { log } from "./logger/index.js";
|
|
17
|
+
|
|
18
|
+
function readAppVersion(): string {
|
|
19
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
20
|
+
try {
|
|
21
|
+
const parsed: unknown = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
22
|
+
if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
|
|
23
|
+
const { version } = parsed as { version: unknown };
|
|
24
|
+
if (typeof version === "string" && version.length > 0) return version;
|
|
25
|
+
}
|
|
26
|
+
log.warn("appVersion", "package.json has no usable version field", { pkgPath });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
log.warn("appVersion", "failed to read package.json", { pkgPath, error: errorMessage(err) });
|
|
29
|
+
}
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Frozen at module load — package.json never changes mid-process. */
|
|
34
|
+
export const APP_VERSION: string = readAppVersion();
|
package/server/system/config.ts
CHANGED
|
@@ -229,6 +229,14 @@ export interface McpStdioSpec {
|
|
|
229
229
|
args?: string[];
|
|
230
230
|
env?: Record<string, string>;
|
|
231
231
|
enabled?: boolean;
|
|
232
|
+
/** Opt-in (#1421 Phase B): when true AND the agent runs in Docker
|
|
233
|
+
* sandbox mode, this stdio server is NOT dropped — instead it is
|
|
234
|
+
* spawned on the HOST behind a stdio↔HTTP gateway and the
|
|
235
|
+
* sandboxed agent reaches it over `host.docker.internal`. This
|
|
236
|
+
* DELIBERATELY escapes the sandbox for this one server; the
|
|
237
|
+
* Settings UI requires an explicit risk acknowledgment to set
|
|
238
|
+
* it. Unset / false → default behavior (dropped + warned). */
|
|
239
|
+
hostExecInDocker?: boolean;
|
|
232
240
|
}
|
|
233
241
|
|
|
234
242
|
export type McpServerSpec = McpHttpSpec | McpStdioSpec;
|
|
@@ -283,6 +291,7 @@ export function isMcpStdioSpec(value: unknown): value is McpStdioSpec {
|
|
|
283
291
|
if (value.args !== undefined && !isStringArray(value.args)) return false;
|
|
284
292
|
if (value.env !== undefined && !isStringRecord(value.env)) return false;
|
|
285
293
|
if (value.enabled !== undefined && typeof value.enabled !== "boolean") return false;
|
|
294
|
+
if (value.hostExecInDocker !== undefined && typeof value.hostExecInDocker !== "boolean") return false;
|
|
286
295
|
return true;
|
|
287
296
|
}
|
|
288
297
|
|
|
@@ -292,10 +301,17 @@ export function isMcpServerSpec(value: unknown): value is McpServerSpec {
|
|
|
292
301
|
|
|
293
302
|
// Workspace id must be slug-shaped so it survives being used as the
|
|
294
303
|
// mcpServers map key and in the `mcp__<id>__<tool>` tool naming.
|
|
304
|
+
//
|
|
305
|
+
// Consecutive `__` is forbidden inside the id because `__` is the
|
|
306
|
+
// delimiter in the tool-name encoding — a server id like `foo__bar`
|
|
307
|
+
// produces `mcp__foo__bar__tool`, which is ambiguous between server
|
|
308
|
+
// `foo` (tool `bar__tool`) and server `foo__bar` (tool `tool`).
|
|
309
|
+
// Forbidding `__` in the id keeps the convention unambiguous
|
|
310
|
+
// everywhere (Codex review on #1356).
|
|
295
311
|
const MCP_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
296
312
|
|
|
297
313
|
export function isMcpServerId(value: unknown): value is string {
|
|
298
|
-
return typeof value === "string" && MCP_ID_RE.test(value);
|
|
314
|
+
return typeof value === "string" && MCP_ID_RE.test(value) && !value.includes("__");
|
|
299
315
|
}
|
|
300
316
|
|
|
301
317
|
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.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Optional host-binary registry + graceful-degradation probe (#1385).
|
|
2
|
+
//
|
|
3
|
+
// A missing optional binary must never crash the app: the feature
|
|
4
|
+
// that needs it disables itself, the user is warned once, and the
|
|
5
|
+
// rest keeps working. This module centralises the "is <binary>
|
|
6
|
+
// available?" question so we don't re-implement the lazy-cached
|
|
7
|
+
// probe per dependency (docker's ad-hoc check was the only one
|
|
8
|
+
// before this).
|
|
9
|
+
|
|
10
|
+
import which from "which";
|
|
11
|
+
import { isDockerLive } from "./docker.js";
|
|
12
|
+
|
|
13
|
+
/** A single optional host dependency the app can run without. */
|
|
14
|
+
export interface OptionalDep {
|
|
15
|
+
/** Stable id used by `depStatus()` lookups and notification ids. */
|
|
16
|
+
readonly id: string;
|
|
17
|
+
/** Binary name passed to `which` for the default presence probe. */
|
|
18
|
+
readonly command: string;
|
|
19
|
+
/** i18n key fragment naming what stops working when absent. */
|
|
20
|
+
readonly enables: string;
|
|
21
|
+
/** Override when "on PATH" is insufficient (e.g. docker client
|
|
22
|
+
* exists but the daemon is down). Returning false means the
|
|
23
|
+
* dependency is treated as unavailable even if `which` found it. */
|
|
24
|
+
readonly probe?: () => Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type DepReason = "ok" | "not-on-path" | "probe-failed";
|
|
28
|
+
|
|
29
|
+
export interface DepStatus {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly available: boolean;
|
|
32
|
+
readonly reason: DepReason;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const REGISTRY: readonly OptionalDep[] = [
|
|
36
|
+
{ id: "docker", command: "docker", enables: "dockerSandbox", probe: isDockerLive },
|
|
37
|
+
{ id: "ffmpeg", command: "ffmpeg", enables: "mulmocast" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
async function onPath(command: string): Promise<boolean> {
|
|
41
|
+
try {
|
|
42
|
+
const resolved = await which(command, { nothrow: true });
|
|
43
|
+
return resolved !== null;
|
|
44
|
+
} catch {
|
|
45
|
+
// `which` shouldn't throw with nothrow, but a corrupt PATH or
|
|
46
|
+
// an fs error must degrade to "absent", never bubble up.
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Never throws — a missing optional dep degrades, it does not crash
|
|
52
|
+
// startup. A throwing PATH check or liveness `probe` is treated as
|
|
53
|
+
// "unavailable" rather than propagating into the boot sequence.
|
|
54
|
+
// `pathCheck` is injectable so unit tests exercise the reason
|
|
55
|
+
// mapping + override precedence without depending on the host's
|
|
56
|
+
// real PATH.
|
|
57
|
+
export async function probeOne(dep: OptionalDep, pathCheck: (command: string) => Promise<boolean> = onPath): Promise<DepStatus> {
|
|
58
|
+
try {
|
|
59
|
+
if (!(await pathCheck(dep.command))) {
|
|
60
|
+
return { id: dep.id, available: false, reason: "not-on-path" };
|
|
61
|
+
}
|
|
62
|
+
if (dep.probe && !(await dep.probe())) {
|
|
63
|
+
return { id: dep.id, available: false, reason: "probe-failed" };
|
|
64
|
+
}
|
|
65
|
+
return { id: dep.id, available: true, reason: "ok" };
|
|
66
|
+
} catch {
|
|
67
|
+
return { id: dep.id, available: false, reason: "probe-failed" };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let cache: Record<string, DepStatus> | null = null;
|
|
72
|
+
let inFlight: Promise<Record<string, DepStatus>> | null = null;
|
|
73
|
+
|
|
74
|
+
/** Probe every registered dependency in parallel. Cached for the
|
|
75
|
+
* process lifetime; concurrent callers share one in-flight run so
|
|
76
|
+
* `which` / the daemon probe fire once each. */
|
|
77
|
+
export async function probeOptionalDeps(): Promise<Record<string, DepStatus>> {
|
|
78
|
+
if (cache) return cache;
|
|
79
|
+
if (inFlight) return inFlight;
|
|
80
|
+
inFlight = (async () => {
|
|
81
|
+
try {
|
|
82
|
+
const statuses = await Promise.all(REGISTRY.map((dep) => probeOne(dep)));
|
|
83
|
+
cache = Object.fromEntries(statuses.map((status) => [status.id, status]));
|
|
84
|
+
return cache;
|
|
85
|
+
} catch {
|
|
86
|
+
// probeOne never rejects, so this is unreachable in practice —
|
|
87
|
+
// but if it ever did, returning an empty map keeps boot alive
|
|
88
|
+
// (callers treat "no status" as "assume available"). Do NOT
|
|
89
|
+
// poison `cache` so a later call can retry.
|
|
90
|
+
return {};
|
|
91
|
+
} finally {
|
|
92
|
+
// Always clear so a rejected/failed run can't permanently
|
|
93
|
+
// poison the cache (Codex review).
|
|
94
|
+
inFlight = null;
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
return inFlight;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Synchronous read of a previously-probed dependency. Returns
|
|
101
|
+
* undefined if `probeOptionalDeps()` has not completed yet — callers
|
|
102
|
+
* on the request path should treat that as "assume available" so a
|
|
103
|
+
* not-yet-probed boot window never blocks a feature. */
|
|
104
|
+
export function depStatus(depId: string): DepStatus | undefined {
|
|
105
|
+
return cache?.[depId];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The registry, for callers that need the human-facing `enables`
|
|
109
|
+
* label (boot warning composition). */
|
|
110
|
+
export function optionalDeps(): readonly OptionalDep[] {
|
|
111
|
+
return REGISTRY;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Test-only: drop the cache so a fresh probe runs next call. */
|
|
115
|
+
export function _resetOptionalDepsCacheForTest(): void {
|
|
116
|
+
cache = null;
|
|
117
|
+
inFlight = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Test-only: seed the probe cache directly so route guards and
|
|
121
|
+
* `announceOptionalDeps` behave deterministically without invoking
|
|
122
|
+
* the real `which` / daemon probe (CI has no control over whether
|
|
123
|
+
* ffmpeg/docker are installed on the runner). `probeOptionalDeps()`
|
|
124
|
+
* returns this seeded cache untouched (its first line is
|
|
125
|
+
* `if (cache) return cache;`). */
|
|
126
|
+
export function _setOptionalDepsCacheForTest(seed: Record<string, DepStatus>): void {
|
|
127
|
+
cache = seed;
|
|
128
|
+
inFlight = null;
|
|
129
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Type declarations for cli-flags.mjs. See the .mjs file for rationale
|
|
2
|
+
// on why the shared registry lives in plain JS.
|
|
3
|
+
|
|
4
|
+
export interface CliFlag {
|
|
5
|
+
flag: string;
|
|
6
|
+
env: string;
|
|
7
|
+
help: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const CLI_FLAGS: readonly CliFlag[];
|
|
11
|
+
|
|
12
|
+
export function flagEnvOverrides(argv: readonly string[]): Record<string, "1">;
|
|
13
|
+
|
|
14
|
+
export function cliFlagHelpLines(): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Shared registry of the boolean CLI flags that mirror an env var.
|
|
2
|
+
// Single source of truth for: the launcher's argv→env injection
|
|
3
|
+
// (`packages/mulmoclaude/bin/mulmoclaude.js`), the launcher `--help`
|
|
4
|
+
// text, and the server-side argv awareness in `server/system/env.ts`.
|
|
5
|
+
//
|
|
6
|
+
// Kept as plain `.mjs` for the same reason as `dev-plugin-args.mjs` /
|
|
7
|
+
// `port.mjs`: the launcher runs BEFORE tsx is wired up, so it can't
|
|
8
|
+
// import from a `.ts` file. Sibling `cli-flags.d.mts` carries the
|
|
9
|
+
// type declarations.
|
|
10
|
+
//
|
|
11
|
+
// Each flag is a launch-time boolean toggle that was previously only
|
|
12
|
+
// reachable via a `VAR=1` env-var prefix — awkward on Windows
|
|
13
|
+
// PowerShell `npx`, in IDE/launcher run configs, and for quick ad-hoc
|
|
14
|
+
// debugging. Setting `<flag>` is equivalent to exporting `<env>=1`;
|
|
15
|
+
// the env var stays supported in parallel (CI scripts / existing docs
|
|
16
|
+
// rely on it). Secret-bearing vars (auth token, API keys) are
|
|
17
|
+
// deliberately NOT here — argv is visible via `ps` and shell history,
|
|
18
|
+
// so those stay env-only. (#1089 + bundle.)
|
|
19
|
+
|
|
20
|
+
/** @type {ReadonlyArray<{ flag: string, env: string, help: string }>} */
|
|
21
|
+
export const CLI_FLAGS = Object.freeze([
|
|
22
|
+
{ flag: "--disable-sandbox", env: "DISABLE_SANDBOX", help: "Run without the Docker sandbox (= DISABLE_SANDBOX=1)" },
|
|
23
|
+
{
|
|
24
|
+
flag: "--disable-macos-reminders",
|
|
25
|
+
env: "DISABLE_MACOS_REMINDER_NOTIFICATIONS",
|
|
26
|
+
help: "Disable the macOS Reminder notification sink (= DISABLE_MACOS_REMINDER_NOTIFICATIONS=1)",
|
|
27
|
+
},
|
|
28
|
+
{ flag: "--persist-tool-calls", env: "PERSIST_TOOL_CALLS", help: "Also persist tool_call events to the session jsonl (= PERSIST_TOOL_CALLS=1)" },
|
|
29
|
+
{ flag: "--journal-force-run", env: "JOURNAL_FORCE_RUN_ON_STARTUP", help: "Run the journal pass immediately on startup (= JOURNAL_FORCE_RUN_ON_STARTUP=1)" },
|
|
30
|
+
{
|
|
31
|
+
flag: "--chat-index-force-run",
|
|
32
|
+
env: "CHAT_INDEX_FORCE_RUN_ON_STARTUP",
|
|
33
|
+
help: "Run the chat-index pass immediately on startup (= CHAT_INDEX_FORCE_RUN_ON_STARTUP=1)",
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Map present CLI flags to the `{ ENV: "1" }` overrides the launcher
|
|
39
|
+
* merges into the spawned server's env. Pure — caller passes argv in.
|
|
40
|
+
*/
|
|
41
|
+
export function flagEnvOverrides(argv) {
|
|
42
|
+
const overrides = {};
|
|
43
|
+
for (const { flag, env } of CLI_FLAGS) {
|
|
44
|
+
if (argv.includes(flag)) overrides[env] = "1";
|
|
45
|
+
}
|
|
46
|
+
return overrides;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Render the aligned `--help` lines for the flag block. */
|
|
50
|
+
export function cliFlagHelpLines() {
|
|
51
|
+
const width = Math.max(...CLI_FLAGS.map(({ flag }) => flag.length));
|
|
52
|
+
return CLI_FLAGS.map(({ flag, help }) => ` ${flag.padEnd(width)} ${help}`).join("\n");
|
|
53
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Single fs gateway for the Encore plugin. All reads / writes
|
|
2
|
+
// against `data/plugins/encore/...` route through here so callers
|
|
3
|
+
// don't sprinkle raw fs / path joins around (CLAUDE.md rule). All
|
|
4
|
+
// writes use `writeFileAtomic` so partial writes can't corrupt the
|
|
5
|
+
// on-disk DSL or cycle state.
|
|
6
|
+
//
|
|
7
|
+
// Domain-agnostic helpers only (read text, write text, read dir,
|
|
8
|
+
// exists, unlink). Markdown frontmatter parsing / serialization
|
|
9
|
+
// lives next to the consumers in src/plugins/encore/.
|
|
10
|
+
|
|
11
|
+
import { promises as fsPromises } from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
import { WORKSPACE_DIRS, WORKSPACE_PATHS } from "../../workspace/paths.js";
|
|
15
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
16
|
+
import { isEnoent } from "./safe.js";
|
|
17
|
+
|
|
18
|
+
/** Absolute path to the Encore plugin's data directory. Reads
|
|
19
|
+
* `WORKSPACE_PATHS.encore` so tests can override the absolute
|
|
20
|
+
* location via Object.defineProperty (same pattern bookmarks
|
|
21
|
+
* integration tests use for `pluginsData`). The optional
|
|
22
|
+
* `workspaceRoot` is a one-off override for callers that want to
|
|
23
|
+
* point at a custom directory without touching the global. */
|
|
24
|
+
export function encoreRoot(workspaceRoot?: string): string {
|
|
25
|
+
if (workspaceRoot !== undefined) return path.join(workspaceRoot, WORKSPACE_DIRS.encore);
|
|
26
|
+
return WORKSPACE_PATHS.encore;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Join a workspace-relative encore path (from paths.ts) to the
|
|
30
|
+
* absolute on-disk location. Validates that the resolved path is
|
|
31
|
+
* still inside the Encore root — `path.join` alone happily
|
|
32
|
+
* resolves `../../..` and escapes the plugin tree if a caller
|
|
33
|
+
* ever passes a traversal-laden segment. paths.ts validates
|
|
34
|
+
* segments at the source, but defense in depth is cheap. */
|
|
35
|
+
function abs(rel: string, workspaceRoot?: string): string {
|
|
36
|
+
const root = encoreRoot(workspaceRoot);
|
|
37
|
+
const resolved = path.resolve(root, rel);
|
|
38
|
+
const normalisedRoot = path.resolve(root);
|
|
39
|
+
if (resolved !== normalisedRoot && !resolved.startsWith(`${normalisedRoot}${path.sep}`)) {
|
|
40
|
+
throw new Error(`encore: path ${JSON.stringify(rel)} escapes the plugin root`);
|
|
41
|
+
}
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read a text file under the Encore tree. Returns `null` if the
|
|
46
|
+
* file doesn't exist (vs. throwing on ENOENT) — callers usually
|
|
47
|
+
* want the missing-file case as "no such obligation" rather than
|
|
48
|
+
* an error. */
|
|
49
|
+
export async function readTextOrNull(rel: string, workspaceRoot?: string): Promise<string | null> {
|
|
50
|
+
try {
|
|
51
|
+
return await fsPromises.readFile(abs(rel, workspaceRoot), "utf8");
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (isEnoent(err)) return null;
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Write text atomically; creates parent directories as needed. */
|
|
59
|
+
export async function writeText(rel: string, content: string, workspaceRoot?: string): Promise<void> {
|
|
60
|
+
const target = abs(rel, workspaceRoot);
|
|
61
|
+
await fsPromises.mkdir(path.dirname(target), { recursive: true });
|
|
62
|
+
await writeFileAtomic(target, content);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** True iff the path exists (file or dir). */
|
|
66
|
+
export async function exists(rel: string, workspaceRoot?: string): Promise<boolean> {
|
|
67
|
+
try {
|
|
68
|
+
await fsPromises.stat(abs(rel, workspaceRoot));
|
|
69
|
+
return true;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (isEnoent(err)) return false;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** List directory entries (basenames only). Empty array if the
|
|
77
|
+
* directory doesn't exist — easier than threading ENOENT through
|
|
78
|
+
* every list call. */
|
|
79
|
+
export async function readDir(rel: string, workspaceRoot?: string): Promise<string[]> {
|
|
80
|
+
try {
|
|
81
|
+
return await fsPromises.readdir(abs(rel, workspaceRoot));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (isEnoent(err)) return [];
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Like `readDir`, but returns only entries that are themselves
|
|
89
|
+
* directories. Used to enumerate obligation IDs without tripping
|
|
90
|
+
* over `.DS_Store` and other stray non-directory entries that
|
|
91
|
+
* macOS and editors drop into the tree. */
|
|
92
|
+
export async function readDirSubdirs(rel: string, workspaceRoot?: string): Promise<string[]> {
|
|
93
|
+
try {
|
|
94
|
+
const entries = await fsPromises.readdir(abs(rel, workspaceRoot), { withFileTypes: true });
|
|
95
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (isEnoent(err)) return [];
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Remove a file. No-op if it doesn't exist (matches the semantics
|
|
103
|
+
* callers want when sweeping orphan tickets). */
|
|
104
|
+
export async function unlink(rel: string, workspaceRoot?: string): Promise<void> {
|
|
105
|
+
try {
|
|
106
|
+
await fsPromises.unlink(abs(rel, workspaceRoot));
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (isEnoent(err)) return;
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
package/server/utils/time.ts
CHANGED
|
@@ -36,6 +36,12 @@ export const DEV_PLUGIN_WATCH_DEBOUNCE_MS = 300;
|
|
|
36
36
|
* a fail-fast guarantee. */
|
|
37
37
|
export const STARTUP_FAILURE_FORCE_EXIT_MS = 5 * ONE_SECOND_MS;
|
|
38
38
|
|
|
39
|
+
/** Tiny grace after an uncaught exception / unhandled rejection so
|
|
40
|
+
* the final `log.error` line flushes to disk before the process
|
|
41
|
+
* bounces. Long enough for a synchronous append, short enough not
|
|
42
|
+
* to delay a crash-restart loop. */
|
|
43
|
+
export const FATAL_LOG_FLUSH_MS = 100;
|
|
44
|
+
|
|
39
45
|
/** Heavy subprocess work (libreoffice conversion, etc.) */
|
|
40
46
|
export const SUBPROCESS_WORK_TIMEOUT_MS = ONE_MINUTE_MS;
|
|
41
47
|
|
|
@@ -37,8 +37,8 @@ Reach for `presentMulmoScript` when the user asks for a presentation, slideshow,
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
|
-
"imageParams": { "provider": "google", "model": "gemini-
|
|
41
|
-
"movieParams": { "provider": "google", "model": "veo-
|
|
40
|
+
"imageParams": { "provider": "google", "model": "gemini-3.1-flash-image-preview" },
|
|
41
|
+
"movieParams": { "provider": "google", "model": "veo-3.1-generate" },
|
|
42
42
|
"textSlideParams": { "cssStyles": "body { background-color: white; }" },
|
|
43
43
|
"beats": [
|
|
44
44
|
{
|