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.
Files changed (182) hide show
  1. package/README.md +26 -0
  2. package/bin/mulmoclaude.js +11 -1
  3. package/client/assets/JsonEditor-D6WBWLoa.js +10 -0
  4. package/client/assets/JsonEditor-Di5xGeZY.css +1 -0
  5. package/client/assets/_plugin-vue_export-helper-BOai-rQB.js +1 -0
  6. package/client/assets/chunk-D8eiyYIV-LcKZGJv5.js +1 -0
  7. package/client/assets/{html2canvas-CDGcmOD3-Bkf2uOth.js → html2canvas-CDGcmOD3-XVrO-eyz.js} +1 -1
  8. package/client/assets/index-CyBr8Mkr.css +2 -0
  9. package/client/assets/index-zZIqEbNX.js +5106 -0
  10. package/client/assets/{index.es-DqtpmBm8-D9mAh_KQ.js → index.es-DqtpmBm8-DHT6q10o.js} +1 -1
  11. package/client/assets/material-symbols-outlined-DtIK7AQn.woff2 +0 -0
  12. package/client/assets/runtime-protocol-vue-D6kcV0wa.js +1 -0
  13. package/client/assets/{runtime-vue-BVUzgYGA.js → runtime-vue-fFYhnNg3.js} +1 -1
  14. package/client/assets/{vue-C8UuIO9J.js → vue-D4w8THF_.js} +1 -1
  15. package/client/assets/vue-i18n-CQbxVmNs.js +3 -0
  16. package/client/assets/vue.runtime.esm-bundler-BTyIdNAI.js +4 -0
  17. package/client/index.html +10 -10
  18. package/package.json +9 -8
  19. package/server/agent/backend/claude-code.ts +34 -0
  20. package/server/agent/backend/fake-echo.ts +370 -0
  21. package/server/agent/backend/index.ts +16 -1
  22. package/server/agent/config.ts +74 -24
  23. package/server/agent/index.ts +104 -80
  24. package/server/agent/mcpFailureMonitor.ts +167 -0
  25. package/server/agent/mcpPreflight.ts +185 -0
  26. package/server/agent/prompt.ts +50 -359
  27. package/server/agent/stdioHttpShim.ts +171 -0
  28. package/server/agent/stream.ts +12 -1
  29. package/server/api/routes/encore.ts +55 -0
  30. package/server/api/routes/files.ts +22 -0
  31. package/server/api/routes/mulmo-script.ts +19 -1
  32. package/server/api/routes/schedulerHandlers.ts +52 -4
  33. package/server/api/routes/sessions.ts +15 -0
  34. package/server/api/routes/skills.ts +263 -0
  35. package/server/build/dispatcher.mjs +299 -0
  36. package/server/encore/INVARIANTS.md +272 -0
  37. package/server/encore/boot.ts +39 -0
  38. package/server/encore/closure.ts +36 -0
  39. package/server/encore/cycle.ts +276 -0
  40. package/server/encore/dispatch.ts +103 -0
  41. package/server/encore/handlers/amend.ts +99 -0
  42. package/server/encore/handlers/appendNote.ts +74 -0
  43. package/server/encore/handlers/defineEncore.ts +42 -0
  44. package/server/encore/handlers/listTickets.ts +107 -0
  45. package/server/encore/handlers/markStepDone.ts +41 -0
  46. package/server/encore/handlers/markTargetSkipped.ts +33 -0
  47. package/server/encore/handlers/query.ts +138 -0
  48. package/server/encore/handlers/recordValues.ts +44 -0
  49. package/server/encore/handlers/resolveNotification.ts +121 -0
  50. package/server/encore/handlers/setup.ts +81 -0
  51. package/server/encore/handlers/shared.ts +137 -0
  52. package/server/encore/handlers/snooze.ts +87 -0
  53. package/server/encore/handlers/startObligationChat.ts +64 -0
  54. package/server/encore/handlers/startSetupChat.ts +50 -0
  55. package/server/encore/lock.ts +61 -0
  56. package/server/encore/notifier.ts +123 -0
  57. package/server/encore/obligation.ts +25 -0
  58. package/server/encore/paths.ts +78 -0
  59. package/server/encore/reconcile.ts +661 -0
  60. package/server/encore/tick.ts +191 -0
  61. package/server/encore/yaml-fm.ts +63 -0
  62. package/server/events/notifications.ts +19 -91
  63. package/server/index.ts +94 -9
  64. package/server/notifier/engine.ts +102 -1
  65. package/server/notifier/macosReminderAdapter.ts +30 -0
  66. package/server/notifier/runtime-api.ts +41 -1
  67. package/server/notifier/types.ts +15 -2
  68. package/server/plugins/runtime.ts +11 -2
  69. package/server/prompts/index.ts +39 -0
  70. package/server/prompts/system/journal-pointer.md +12 -0
  71. package/server/prompts/system/memory-management-atomic.md +33 -0
  72. package/server/prompts/system/memory-management-topic.md +60 -0
  73. package/server/prompts/system/news-concierge.md +24 -0
  74. package/server/prompts/system/sandbox-tools.md +10 -0
  75. package/server/prompts/system/sources-context.md +16 -0
  76. package/server/prompts/system/system.md +91 -0
  77. package/server/system/announceOptionalDeps.ts +57 -0
  78. package/server/system/appVersion.ts +34 -0
  79. package/server/system/config.ts +17 -1
  80. package/server/system/docker.ts +14 -6
  81. package/server/system/env.ts +18 -5
  82. package/server/system/optionalDeps.ts +129 -0
  83. package/server/utils/cli-flags.d.mts +14 -0
  84. package/server/utils/cli-flags.mjs +53 -0
  85. package/server/utils/files/encore-io.ts +111 -0
  86. package/server/utils/time.ts +6 -0
  87. package/server/workspace/helps/business.md +2 -2
  88. package/server/workspace/helps/encore-dsl.md +482 -0
  89. package/server/workspace/helps/index.md +15 -13
  90. package/server/workspace/helps/mulmoscript.md +3 -3
  91. package/server/workspace/helps/sandbox.md +2 -2
  92. package/server/workspace/hooks/dispatcher.ts +7 -5
  93. package/server/workspace/hooks/provision.ts +6 -3
  94. package/server/workspace/paths.ts +13 -4
  95. package/server/workspace/skills/catalog.ts +355 -0
  96. package/server/workspace/skills/external/catalog.ts +283 -0
  97. package/server/workspace/skills/external/clone.ts +129 -0
  98. package/server/workspace/skills/external/id.ts +194 -0
  99. package/server/workspace/skills/external/install.ts +417 -0
  100. package/server/workspace/skills/external/presets.ts +50 -0
  101. package/server/workspace/skills-preset.ts +29 -17
  102. package/server/workspace/workspace.ts +10 -5
  103. package/src/App.vue +37 -8
  104. package/src/components/FileContentRenderer.vue +102 -9
  105. package/src/components/JsonEditor.vue +160 -0
  106. package/src/components/NotificationBell.vue +35 -3
  107. package/src/components/PluginLauncher.vue +20 -41
  108. package/src/components/RightSidebar.vue +19 -0
  109. package/src/components/SettingsMcpTab.vue +58 -11
  110. package/src/components/SettingsModal.vue +22 -1
  111. package/src/components/StackView.vue +10 -1
  112. package/src/components/TodoExplorer.vue +16 -0
  113. package/src/components/todo/TodoKanbanView.vue +34 -6
  114. package/src/composables/useNotifications.ts +21 -1
  115. package/src/config/apiRoutes.ts +0 -6
  116. package/src/config/mcpCatalog.ts +12 -7
  117. package/src/config/mcpTypes.ts +5 -0
  118. package/src/config/roles.ts +52 -15
  119. package/src/config/systemFileDescriptors.ts +12 -0
  120. package/src/lang/de.ts +108 -12
  121. package/src/lang/en.ts +105 -11
  122. package/src/lang/es.ts +106 -11
  123. package/src/lang/fr.ts +106 -11
  124. package/src/lang/ja.ts +104 -11
  125. package/src/lang/ko.ts +105 -11
  126. package/src/lang/pt-BR.ts +106 -11
  127. package/src/lang/zh.ts +103 -11
  128. package/src/main.ts +1 -0
  129. package/src/plugins/_generated/metas.ts +4 -0
  130. package/src/plugins/_generated/registrations.ts +2 -0
  131. package/src/plugins/_generated/server-bindings.ts +5 -0
  132. package/src/plugins/encore/EncoreDashboard.vue +504 -0
  133. package/src/plugins/encore/EncoreRedirect.vue +116 -0
  134. package/src/plugins/encore/View.vue +36 -0
  135. package/src/plugins/encore/defineEncoreDefinition.ts +74 -0
  136. package/src/plugins/encore/defineEncoreMeta.ts +13 -0
  137. package/src/plugins/encore/index.ts +93 -0
  138. package/src/plugins/encore/manageEncoreDefinition.ts +100 -0
  139. package/src/plugins/encore/manageEncoreMeta.ts +36 -0
  140. package/src/plugins/manageSkills/View.vue +832 -30
  141. package/src/plugins/manageSkills/categories.ts +125 -0
  142. package/src/plugins/manageSkills/meta.ts +30 -0
  143. package/src/plugins/markdown/definition.ts +3 -3
  144. package/src/plugins/meta-types.ts +5 -0
  145. package/src/plugins/presentMulmoScript/Preview.vue +3 -3
  146. package/src/plugins/presentMulmoScript/View.vue +157 -33
  147. package/src/plugins/presentMulmoScript/meta.ts +4 -0
  148. package/src/plugins/scheduler/View.vue +45 -9
  149. package/src/plugins/scheduler/calendarDefinition.ts +6 -2
  150. package/src/plugins/scheduler/multiDayHelpers.ts +95 -0
  151. package/src/plugins/skill/View.vue +1 -5
  152. package/src/plugins/spreadsheet/View.vue +3 -3
  153. package/src/plugins/spreadsheet/definition.ts +1 -1
  154. package/src/plugins/textResponse/Preview.vue +14 -1
  155. package/src/plugins/textResponse/View.vue +39 -24
  156. package/src/plugins/wiki/components/WikiPageBody.vue +4 -0
  157. package/src/router/index.ts +11 -0
  158. package/src/router/pageRoutes.ts +1 -0
  159. package/src/types/encore-dsl/at-expression.ts +120 -0
  160. package/src/types/encore-dsl/at-resolver.ts +32 -0
  161. package/src/types/encore-dsl/cadence.ts +289 -0
  162. package/src/types/encore-dsl/schema.ts +288 -0
  163. package/src/types/notification.ts +2 -1
  164. package/src/types/session.ts +6 -0
  165. package/src/types/sse.ts +5 -0
  166. package/src/types/toolCallHistory.ts +7 -0
  167. package/src/utils/agent/eventDispatch.ts +26 -5
  168. package/src/utils/agent/mcpHint.ts +50 -0
  169. package/src/utils/image/htmlSrcAttrs.ts +117 -13
  170. package/src/utils/session/sessionEntries.ts +8 -32
  171. package/client/assets/PluginScopedRoot-YjvQq0Nn.js +0 -3
  172. package/client/assets/chunk-CernVdwh.js +0 -1
  173. package/client/assets/chunk-D8eiyYIV-CAXpUwLd.js +0 -1
  174. package/client/assets/index-BwrlMMHr.js +0 -5005
  175. package/client/assets/index-CvvNuegU.css +0 -2
  176. package/client/assets/material-symbols-outlined-BOZVWuR3.woff2 +0 -0
  177. package/client/assets/runtime-protocol-vue-C1To4M3t.js +0 -1
  178. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +0 -4
  179. package/server/api/routes/notifications.ts +0 -195
  180. package/server/notifier/legacy-adapters.ts +0 -76
  181. package/server/workspace/hooks/dispatcher.mjs +0 -300
  182. 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();
@@ -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 {
@@ -41,18 +41,26 @@ function assertClaudeFiles(): void {
41
41
  }
42
42
  }
43
43
 
44
- export async function isDockerAvailable(): Promise<boolean> {
45
- if (env.disableSandbox) return false;
46
- if (_dockerEnabled !== null) return _dockerEnabled;
47
- assertClaudeFiles();
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
- _dockerEnabled = true;
53
+ return true;
53
54
  } catch {
54
- _dockerEnabled = false;
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
 
@@ -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: asFlag(process.env.DISABLE_SANDBOX),
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: asFlag(process.env.PERSIST_TOOL_CALLS),
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: asFlag(process.env.JOURNAL_FORCE_RUN_ON_STARTUP),
93
- chatIndexForceRunOnStartup: asFlag(process.env.CHAT_INDEX_FORCE_RUN_ON_STARTUP),
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: asFlag(process.env.DISABLE_MACOS_REMINDER_NOTIFICATIONS),
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
+ }
@@ -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-2.5-flash-image" },
41
- "movieParams": { "provider": "google", "model": "veo-2.0-generate-001" },
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
  {