mulmoclaude 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mulmoclaude.js +7 -24
- package/client/assets/html2canvas-Cx501zZr-DiKaqnKs.js +5 -0
- package/client/assets/{index-eHWB79u5.js → index-C94GcmNa.js} +203 -198
- package/client/assets/index-CY-WpQUm.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-5ipqh8Pe.js} +5 -5
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/client/index.html +2 -4
- package/package.json +17 -15
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/backend/claude-code.ts +170 -0
- package/server/agent/backend/index.ts +14 -0
- package/server/agent/backend/types.ts +65 -0
- package/server/agent/index.ts +31 -159
- package/server/agent/mcp-server.ts +88 -10
- package/server/agent/mcp-tools/index.ts +8 -7
- package/server/agent/mcp-tools/notify.ts +76 -0
- package/server/agent/mcp-tools/x.ts +12 -2
- package/server/agent/plugin-names.ts +10 -4
- package/server/agent/prompt.ts +187 -26
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +3 -3
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +99 -4
- package/server/api/routes/chart.ts +13 -0
- package/server/api/routes/chat-index.ts +2 -1
- package/server/api/routes/config.ts +35 -8
- package/server/api/routes/files.ts +75 -24
- package/server/api/routes/html.ts +15 -2
- package/server/api/routes/image.ts +75 -20
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/news.ts +146 -0
- package/server/api/routes/notifications.ts +58 -2
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +73 -91
- package/server/api/routes/presentHtml.ts +9 -0
- package/server/api/routes/roles.ts +12 -2
- package/server/api/routes/scheduler.ts +20 -11
- package/server/api/routes/schedulerTasks.ts +58 -21
- package/server/api/routes/sessions.ts +15 -4
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +26 -5
- package/server/api/routes/sources.ts +8 -7
- package/server/api/routes/todos.ts +30 -0
- package/server/api/routes/todosColumnsHandlers.ts +13 -27
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki/frontmatter.ts +86 -0
- package/server/api/routes/wiki.ts +335 -75
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +32 -8
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +26 -16
- package/server/events/resolveRelayBridgeOptions.ts +125 -0
- package/server/index.ts +72 -49
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +15 -5
- package/server/system/macosNotify.ts +152 -0
- package/server/utils/errors.ts +11 -2
- package/server/utils/fetch.ts +54 -0
- package/server/utils/files/atomic.ts +18 -17
- package/server/utils/files/image-store.ts +19 -13
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-image-fill.ts +131 -0
- package/server/utils/files/markdown-store.ts +22 -6
- package/server/utils/files/naming.ts +20 -10
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +4 -4
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/spreadsheet-store.ts +15 -10
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +30 -4
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/id.ts +40 -8
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/logPreview.ts +24 -0
- package/server/utils/markdown.ts +5 -5
- package/server/utils/port.d.mts +6 -0
- package/server/utils/port.mjs +48 -0
- package/server/utils/promptMeta.ts +32 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/slug.ts +65 -4
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/index.ts +1 -1
- package/server/workspace/chat-index/summarizer.ts +5 -5
- package/server/workspace/custom-dirs.ts +5 -5
- package/server/workspace/helps/gemini.md +57 -0
- package/server/workspace/helps/index.md +2 -1
- package/server/workspace/helps/sources.md +42 -0
- package/server/workspace/helps/wiki.md +40 -5
- package/server/workspace/journal/archivist-cli.ts +121 -0
- package/server/workspace/journal/{archivist.ts → archivist-schemas.ts} +12 -120
- package/server/workspace/journal/dailyPass.ts +78 -38
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +56 -5
- package/server/workspace/journal/memoryExtractor.ts +1 -1
- package/server/workspace/journal/optimizationPass.ts +4 -5
- package/server/workspace/journal/paths.ts +8 -24
- package/server/workspace/journal/state.ts +18 -8
- package/server/workspace/news/reader.ts +248 -0
- package/server/workspace/paths.ts +4 -3
- package/server/workspace/reference-dirs.ts +3 -3
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +5 -4
- package/server/workspace/skills/user-tasks.ts +3 -2
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +2 -2
- package/server/workspace/sources/classifier.ts +1 -1
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +3 -3
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +59 -13
- package/server/workspace/sources/pipeline/index.ts +59 -7
- package/server/workspace/sources/pipeline/notify.ts +13 -5
- package/server/workspace/sources/pipeline/plan.ts +11 -9
- package/server/workspace/sources/pipeline/summarize.ts +1 -1
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/sourceState.ts +9 -4
- package/server/workspace/sources/types.ts +9 -0
- package/server/workspace/sources/urls.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +4 -4
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +477 -251
- package/src/components/CanvasViewToggle.vue +12 -10
- package/src/components/ChatInput.vue +112 -105
- package/src/components/FileContentHeader.vue +10 -7
- package/src/components/FileContentRenderer.vue +37 -10
- package/src/components/FileTree.vue +34 -4
- package/src/components/FileTreePane.vue +32 -27
- package/src/components/FilesView.vue +5 -3
- package/src/components/FilterChip.vue +22 -0
- package/src/components/LockStatusPopup.vue +19 -13
- package/src/components/NewsView.vue +252 -0
- package/src/components/NotificationBell.vue +35 -9
- package/src/components/NotificationToast.vue +4 -1
- package/src/components/PageChatComposer.vue +101 -0
- package/src/components/PluginLauncher.vue +36 -62
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +3 -2
- package/src/components/SessionHeaderControls.vue +63 -0
- package/src/components/SessionHistoryExpandButton.vue +30 -0
- package/src/components/SessionHistoryPanel.vue +64 -93
- package/src/components/SessionHistoryToggleButton.vue +40 -0
- package/src/components/SessionRoleIcon.vue +72 -0
- package/src/components/SessionSidebar.vue +96 -0
- package/src/components/SessionTabBar.vue +44 -51
- package/src/components/SettingsMcpTab.vue +361 -52
- package/src/components/SettingsModal.vue +203 -72
- package/src/components/SettingsReferenceDirsTab.vue +72 -51
- package/src/components/SettingsWorkspaceDirsTab.vue +74 -51
- package/src/components/SidebarHeader.vue +50 -16
- package/src/components/SourcesManager.vue +900 -0
- package/src/components/SourcesView.vue +45 -0
- package/src/components/StackView.vue +84 -48
- package/src/components/SuggestionsPanel.vue +25 -36
- package/src/components/SystemFileBanner.vue +106 -0
- package/src/components/ThinkingIndicator.vue +41 -0
- package/src/components/TodoExplorer.vue +72 -22
- package/src/components/todo/TodoAddDialog.vue +17 -12
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +15 -10
- package/src/components/todo/TodoKanbanView.vue +16 -6
- package/src/components/todo/TodoListView.vue +14 -3
- package/src/components/todo/TodoTableView.vue +36 -5
- package/src/composables/favicon/conditions.ts +76 -0
- package/src/composables/favicon/resolveColor.ts +93 -0
- package/src/composables/favicon/types.ts +61 -0
- package/src/composables/useAppApi.ts +23 -0
- package/src/composables/useChatScroll.ts +5 -5
- package/src/composables/useCurrentRole.ts +32 -0
- package/src/composables/useDynamicFavicon.ts +174 -58
- package/src/composables/useEventListeners.ts +7 -12
- package/src/composables/useFaviconState.ts +93 -12
- package/src/composables/useFileSelection.ts +25 -6
- package/src/composables/useHealth.ts +76 -7
- package/src/composables/useLayoutMode.ts +27 -0
- package/src/composables/useNewsItems.ts +38 -0
- package/src/composables/useNewsReadState.ts +75 -0
- package/src/composables/useNotifications.ts +76 -13
- package/src/composables/usePendingCalls.ts +11 -1
- package/src/composables/useRoles.ts +6 -10
- package/src/composables/useRunElapsed.ts +80 -0
- package/src/composables/useSessionDerived.ts +21 -5
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSidePanelVisible.ts +25 -0
- package/src/composables/useViewLayout.ts +16 -37
- package/src/config/apiRoutes.ts +19 -6
- package/src/config/historyFilters.ts +30 -0
- package/src/config/mcpCatalog.ts +285 -0
- package/src/config/mcpTypes.ts +26 -0
- package/src/config/roles.ts +19 -51
- package/src/config/systemFileDescriptors.ts +170 -0
- package/src/config/toolNames.ts +6 -1
- package/src/config/workspacePaths.ts +1 -0
- package/src/index.css +14 -0
- package/src/lang/de.ts +706 -0
- package/src/lang/en.ts +726 -0
- package/src/lang/es.ts +712 -0
- package/src/lang/fr.ts +704 -0
- package/src/lang/ja.ts +707 -0
- package/src/lang/ko.ts +709 -0
- package/src/lang/pt-BR.ts +702 -0
- package/src/lang/zh.ts +705 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +3 -0
- package/src/plugins/canvas/View.vue +104 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/canvas/index.ts +3 -2
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/chart/index.ts +3 -2
- package/src/plugins/editImage/index.ts +3 -2
- package/src/plugins/generateImage/index.ts +3 -2
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +67 -46
- package/src/plugins/manageRoles/index.ts +3 -2
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +39 -34
- package/src/plugins/manageSkills/index.ts +3 -2
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/manageSource/View.vue +3 -687
- package/src/plugins/manageSource/index.ts +3 -2
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +164 -73
- package/src/plugins/markdown/definition.ts +6 -4
- package/src/plugins/markdown/index.ts +3 -2
- package/src/plugins/presentForm/Preview.vue +99 -0
- package/src/plugins/presentForm/View.vue +675 -0
- package/src/plugins/presentForm/definition.ts +127 -0
- package/src/plugins/presentForm/index.ts +18 -0
- package/src/plugins/presentForm/plugin.ts +94 -0
- package/src/plugins/presentForm/types.ts +109 -0
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/index.ts +3 -2
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +36 -26
- package/src/plugins/presentMulmoScript/index.ts +3 -2
- package/src/plugins/scheduler/AutomationsPreview.vue +37 -0
- package/src/plugins/scheduler/AutomationsView.vue +23 -0
- package/src/plugins/scheduler/CalendarView.vue +23 -0
- package/src/plugins/scheduler/LegacySchedulerView.vue +32 -0
- package/src/plugins/scheduler/Preview.vue +7 -4
- package/src/plugins/scheduler/TasksTab.vue +119 -28
- package/src/plugins/scheduler/View.vue +75 -32
- package/src/plugins/scheduler/automationsDefinition.ts +58 -0
- package/src/plugins/scheduler/calendarDefinition.ts +46 -0
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/scheduler/index.ts +68 -14
- package/src/plugins/scheduler/legacyShape.ts +34 -0
- package/src/plugins/spreadsheet/Preview.vue +9 -5
- package/src/plugins/spreadsheet/View.vue +43 -57
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +2 -1
- package/src/plugins/spreadsheet/index.ts +3 -2
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +42 -45
- package/src/plugins/textResponse/utils.ts +25 -0
- package/src/plugins/todo/Preview.vue +11 -6
- package/src/plugins/todo/View.vue +27 -13
- package/src/plugins/todo/composables/useTodos.ts +3 -1
- package/src/plugins/todo/index.ts +3 -2
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +5 -2
- package/src/plugins/wiki/View.vue +539 -92
- package/src/plugins/wiki/index.ts +5 -2
- package/src/plugins/wiki/route.ts +121 -0
- package/src/router/guards.ts +43 -24
- package/src/router/index.ts +53 -26
- package/src/router/pageRoutes.ts +23 -0
- package/src/tools/index.ts +12 -5
- package/src/tools/legacyPluginNames.ts +13 -0
- package/src/types/notification.ts +31 -6
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/eventDispatch.ts +3 -6
- package/src/utils/agent/formatElapsed.ts +37 -0
- package/src/utils/agent/request.ts +22 -1
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/canvas/sidePanelVisible.ts +19 -0
- package/src/utils/dom/scrollIntoViewByTestId.ts +38 -0
- package/src/utils/errors.ts +9 -2
- package/src/utils/files/filename.ts +24 -0
- package/src/utils/filesPreview/schedulerPreview.ts +9 -3
- package/src/utils/id.ts +18 -0
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/markdown/taskList.ts +175 -0
- package/src/utils/mcp/interpolateSpec.ts +97 -0
- package/src/utils/notification/dispatch.ts +51 -15
- package/src/utils/path/workspaceLinkRouter.ts +99 -0
- package/src/utils/session/mergeSessions.ts +5 -0
- package/src/utils/sources/filter.ts +69 -0
- package/src/vite-env.d.ts +9 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
- package/client/assets/index-Bm70FDU2.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/server/workspace/journal/linkRewrite.ts +0 -4
- package/src/components/ToolResultsPanel.vue +0 -77
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/plugins/scheduler/definition.ts +0 -57
- package/src/utils/canvas/viewMode.ts +0 -46
- package/src/utils/role/plugins.ts +0 -12
- package/src/utils/session/seedRoleDefault.ts +0 -35
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
|
@@ -15,24 +15,24 @@
|
|
|
15
15
|
// the workspace scale (~hundreds of dirs) this is negligible. If
|
|
16
16
|
// profiling shows otherwise, cache the parsed ignore instances.
|
|
17
17
|
|
|
18
|
-
import
|
|
18
|
+
import { readFileSync } from "fs";
|
|
19
19
|
import path from "path";
|
|
20
20
|
import ignore, { type Ignore } from "ignore";
|
|
21
21
|
|
|
22
22
|
export class GitignoreFilter {
|
|
23
|
-
private
|
|
23
|
+
private rules: Ignore;
|
|
24
24
|
|
|
25
25
|
constructor(rules?: string) {
|
|
26
|
-
this.
|
|
26
|
+
this.rules = ignore();
|
|
27
27
|
if (rules) {
|
|
28
|
-
this.
|
|
28
|
+
this.rules.add(rules);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** Test whether a workspace-relative path should be hidden. */
|
|
33
33
|
ignores(relPath: string): boolean {
|
|
34
34
|
if (!relPath) return false;
|
|
35
|
-
return this.
|
|
35
|
+
return this.rules.ignores(relPath);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** Create a child filter that inherits this filter's rules and
|
|
@@ -40,12 +40,12 @@ export class GitignoreFilter {
|
|
|
40
40
|
childForDir(dirAbsPath: string): GitignoreFilter {
|
|
41
41
|
const child = new GitignoreFilter();
|
|
42
42
|
// Inherit parent rules
|
|
43
|
-
child.
|
|
43
|
+
child.rules = ignore().add(this.rules);
|
|
44
44
|
// Add local .gitignore if present
|
|
45
45
|
const gitignorePath = path.join(dirAbsPath, ".gitignore");
|
|
46
46
|
try {
|
|
47
|
-
const content =
|
|
48
|
-
child.
|
|
47
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
48
|
+
child.rules.add(content);
|
|
49
49
|
} catch {
|
|
50
50
|
// No .gitignore in this directory — just inherit parent
|
|
51
51
|
}
|
|
@@ -61,7 +61,7 @@ export class GitignoreFilter {
|
|
|
61
61
|
export function createRootFilter(workspaceRoot: string): GitignoreFilter {
|
|
62
62
|
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
63
63
|
try {
|
|
64
|
-
const content =
|
|
64
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
65
65
|
return new GitignoreFilter(content);
|
|
66
66
|
} catch {
|
|
67
67
|
return new GitignoreFilter();
|
package/server/utils/id.ts
CHANGED
|
@@ -1,16 +1,48 @@
|
|
|
1
|
-
// Unique-ID generation
|
|
2
|
-
//
|
|
3
|
-
// consolidated here as part of the server/utils grouping (#350 CLAUDE.md).
|
|
1
|
+
// Unique-ID generation helpers. Three layers, one primitive
|
|
2
|
+
// (`crypto.randomUUID()`). See issue #723 for the design rationale.
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
|
|
6
|
+
// 16-char hex slug length for `shortId()`. 64 bits of entropy —
|
|
7
|
+
// ~10^9 generations before a 1% collision rate — which is plenty
|
|
8
|
+
// for filename suffixes.
|
|
9
|
+
const SHORT_ID_HEX_LEN = 16;
|
|
10
|
+
// 6 hex chars for `makeId()`'s random tail — the timestamp already
|
|
11
|
+
// carries most of the uniqueness, so 24 bits of extra entropy is
|
|
12
|
+
// enough to disambiguate IDs generated in the same millisecond.
|
|
13
|
+
const MAKE_ID_HEX_LEN = 6;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Full UUID v4 (36 chars, hyphenated).
|
|
17
|
+
*
|
|
18
|
+
* Use when the id is globally unique across the app lifetime and
|
|
19
|
+
* round-trips through URLs, jsonl files, or external systems that
|
|
20
|
+
* already expect the v4 shape (e.g. `chatSessionId`, scheduler
|
|
21
|
+
* `task.id`, notification ids).
|
|
22
|
+
*/
|
|
23
|
+
export function makeUuid(): string {
|
|
24
|
+
return randomUUID();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 16-char hex slug derived from a UUID v4.
|
|
29
|
+
*
|
|
30
|
+
* Use when a short, opaque identifier is sufficient — e.g. image
|
|
31
|
+
* and spreadsheet file-name suffixes. Not suitable for IDs that
|
|
32
|
+
* round-trip through systems expecting UUID v4 formatting.
|
|
33
|
+
*/
|
|
34
|
+
export function shortId(): string {
|
|
35
|
+
return randomUUID().replace(/-/g, "").slice(0, SHORT_ID_HEX_LEN);
|
|
36
|
+
}
|
|
6
37
|
|
|
7
38
|
/**
|
|
8
|
-
*
|
|
39
|
+
* Domain-prefixed, human-scannable ID.
|
|
9
40
|
*
|
|
10
41
|
* Format: `<prefix>_<epochMs>_<6 random hex chars>`. The prefix
|
|
11
|
-
*
|
|
12
|
-
*
|
|
42
|
+
* makes IDs from different domains (todo, scheduler, column)
|
|
43
|
+
* visually distinguishable in logs and JSON files.
|
|
13
44
|
*/
|
|
14
45
|
export function makeId(prefix: string): string {
|
|
15
|
-
|
|
46
|
+
const randomHex = randomUUID().replace(/-/g, "").slice(0, MAKE_ID_HEX_LEN);
|
|
47
|
+
return `${prefix}_${Date.now()}_${randomHex}`;
|
|
16
48
|
}
|
package/server/utils/json.ts
CHANGED
|
@@ -44,22 +44,22 @@ export function findBalancedBraceBlock(raw: string): string | null {
|
|
|
44
44
|
let inString = false;
|
|
45
45
|
let escape = false;
|
|
46
46
|
for (let i = start; i < raw.length; i++) {
|
|
47
|
-
const
|
|
47
|
+
const char = raw[i];
|
|
48
48
|
if (escape) {
|
|
49
49
|
escape = false;
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
|
-
if (
|
|
52
|
+
if (char === "\\") {
|
|
53
53
|
escape = true;
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
|
-
if (
|
|
56
|
+
if (char === '"') {
|
|
57
57
|
inString = !inString;
|
|
58
58
|
continue;
|
|
59
59
|
}
|
|
60
60
|
if (inString) continue;
|
|
61
|
-
if (
|
|
62
|
-
if (
|
|
61
|
+
if (char === "{") depth++;
|
|
62
|
+
if (char === "}" && --depth === 0) return raw.slice(start, i + 1);
|
|
63
63
|
}
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { log } from "../system/logger/index.js";
|
|
2
|
+
import { errorMessage } from "./errors.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Build a `.catch` handler for a fire-and-forget background job that
|
|
@@ -8,15 +9,23 @@ import { log } from "../system/logger/index.js";
|
|
|
8
9
|
*
|
|
9
10
|
* Usage:
|
|
10
11
|
*
|
|
12
|
+
* // Default message — good for generic background scans.
|
|
11
13
|
* maybeRunJournal({ ... }).catch(logBackgroundError("journal"));
|
|
12
14
|
*
|
|
15
|
+
* // Custom message — pass context when the failure mode is
|
|
16
|
+
* // worth surfacing distinctly (e.g. "failed to register
|
|
17
|
+
* // scheduled skills" vs other `skills` warnings).
|
|
18
|
+
* registerScheduledSkills(...).catch(
|
|
19
|
+
* logBackgroundError("skills", "failed to register scheduled skills"),
|
|
20
|
+
* );
|
|
21
|
+
*
|
|
13
22
|
* The handler never rethrows — the caller's promise chain is
|
|
14
23
|
* terminated cleanly so nothing propagates into the request path.
|
|
15
24
|
*/
|
|
16
|
-
export function logBackgroundError(prefix: string): (err: unknown) => void {
|
|
25
|
+
export function logBackgroundError(prefix: string, message = "unexpected error in background"): (err: unknown) => void {
|
|
17
26
|
return (err) => {
|
|
18
|
-
log.warn(prefix,
|
|
19
|
-
error:
|
|
27
|
+
log.warn(prefix, message, {
|
|
28
|
+
error: errorMessage(err),
|
|
20
29
|
});
|
|
21
30
|
};
|
|
22
31
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Truncating preview for log payloads.
|
|
2
|
+
//
|
|
3
|
+
// Logs of user-supplied freeform text (prompts, wiki bodies, search
|
|
4
|
+
// queries) must never include the full body — partly to keep log
|
|
5
|
+
// files small, partly to limit accidental PII / secret leakage when
|
|
6
|
+
// logs are shared during debugging. 120 chars + an ellipsis is the
|
|
7
|
+
// shape the image-generation logging (PR #780) settled on; this
|
|
8
|
+
// module exists so other routes use the same constant rather than
|
|
9
|
+
// hand-rolling their own cap.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// log.info("wiki", "page: start", { pageNamePreview: previewSnippet(pageName) });
|
|
13
|
+
//
|
|
14
|
+
// `null` / `undefined` → empty string, so the logger never has to
|
|
15
|
+
// guard against missing input.
|
|
16
|
+
|
|
17
|
+
const PREVIEW_CHAR_LIMIT = 120;
|
|
18
|
+
const ELLIPSIS = "…";
|
|
19
|
+
|
|
20
|
+
export function previewSnippet(input: string | null | undefined): string {
|
|
21
|
+
if (!input) return "";
|
|
22
|
+
if (input.length <= PREVIEW_CHAR_LIMIT) return input;
|
|
23
|
+
return `${input.slice(0, PREVIEW_CHAR_LIMIT)}${ELLIPSIS}`;
|
|
24
|
+
}
|
package/server/utils/markdown.ts
CHANGED
|
@@ -68,15 +68,15 @@ export function rewriteMarkdownLinks(input: string, rewrite: (href: string) => s
|
|
|
68
68
|
* Split a trailing `#fragment` or `?query` off a path so the caller
|
|
69
69
|
* can rewrite the path portion and concatenate the suffix back.
|
|
70
70
|
*/
|
|
71
|
-
export function splitFragmentAndQuery(
|
|
71
|
+
export function splitFragmentAndQuery(href: string): {
|
|
72
72
|
pathPart: string;
|
|
73
73
|
suffix: string;
|
|
74
74
|
} {
|
|
75
|
-
const hashIdx =
|
|
76
|
-
const queryIdx =
|
|
75
|
+
const hashIdx = href.indexOf("#");
|
|
76
|
+
const queryIdx = href.indexOf("?");
|
|
77
77
|
let cut = -1;
|
|
78
78
|
if (hashIdx !== -1) cut = hashIdx;
|
|
79
79
|
if (queryIdx !== -1 && (cut === -1 || queryIdx < cut)) cut = queryIdx;
|
|
80
|
-
if (cut === -1) return { pathPart:
|
|
81
|
-
return { pathPart:
|
|
80
|
+
if (cut === -1) return { pathPart: href, suffix: "" };
|
|
81
|
+
return { pathPart: href.slice(0, cut), suffix: href.slice(cut) };
|
|
82
82
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Type declarations for port.mjs. See port.mjs for rationale on why
|
|
2
|
+
// the shared helper lives in plain JS.
|
|
3
|
+
|
|
4
|
+
export const MAX_PORT_PROBES: number;
|
|
5
|
+
export function isPortFree(port: number): Promise<boolean>;
|
|
6
|
+
export function findAvailablePort(start: number): Promise<number | null>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Shared port-resolution helpers for the dev server and the npm
|
|
2
|
+
// launcher (`packages/mulmoclaude/bin/mulmoclaude.js`).
|
|
3
|
+
//
|
|
4
|
+
// Kept as plain `.mjs` (no TypeScript) because the launcher runs
|
|
5
|
+
// directly under Node — it boots BEFORE tsx is wired up, so it can't
|
|
6
|
+
// import from a `.ts` file. The server-side TypeScript imports this
|
|
7
|
+
// through Node's ESM resolution; `moduleResolution: "bundler"` + the
|
|
8
|
+
// sibling `port.d.mts` declarations give us the type coverage without
|
|
9
|
+
// turning the whole `server/` tree into mixed JS/TS.
|
|
10
|
+
|
|
11
|
+
import net from "node:net";
|
|
12
|
+
|
|
13
|
+
// Scan cap: 20 slots is enough to step around the occasional stale
|
|
14
|
+
// server on `localhost` without spinning forever on a pathologically
|
|
15
|
+
// saturated machine.
|
|
16
|
+
export const MAX_PORT_PROBES = 20;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns true iff binding `127.0.0.1:port` would succeed right now.
|
|
20
|
+
* @param {number} port
|
|
21
|
+
* @returns {Promise<boolean>}
|
|
22
|
+
*/
|
|
23
|
+
export function isPortFree(port) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const server = net.createServer();
|
|
26
|
+
server.once("error", () => resolve(false));
|
|
27
|
+
server.once("listening", () => {
|
|
28
|
+
server.close(() => resolve(true));
|
|
29
|
+
});
|
|
30
|
+
// Probe the same interface we'll actually bind to so a port
|
|
31
|
+
// held by a different process on a different interface doesn't
|
|
32
|
+
// give us a false "free" reading.
|
|
33
|
+
server.listen(port, "127.0.0.1");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Walk forward from `start` until we find a free port. Returns `null`
|
|
39
|
+
* if every slot in `[start, start + MAX_PORT_PROBES)` is busy.
|
|
40
|
+
* @param {number} start
|
|
41
|
+
* @returns {Promise<number | null>}
|
|
42
|
+
*/
|
|
43
|
+
export async function findAvailablePort(start) {
|
|
44
|
+
for (let candidate = start; candidate < start + MAX_PORT_PROBES; candidate++) {
|
|
45
|
+
if (await isPortFree(candidate)) return candidate;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Fingerprint user-controlled prompt text for structured logs without
|
|
2
|
+
// leaking the prompt itself.
|
|
3
|
+
//
|
|
4
|
+
// Image / text prompts are user input and routinely include pasted
|
|
5
|
+
// URLs, emails, or even credentials. Persisting any prefix of the
|
|
6
|
+
// raw prompt into structured log files (which we keep on disk and
|
|
7
|
+
// rotate) is a PII / secret leak waiting to happen.
|
|
8
|
+
//
|
|
9
|
+
// The fingerprint pairs `length` with a 12-hex-char prefix of the
|
|
10
|
+
// SHA-256 hash. 48 bits of entropy is enough to correlate retries
|
|
11
|
+
// and duplicate-request bursts within a session — collision risk
|
|
12
|
+
// at session-scale request volumes is negligible — without exposing
|
|
13
|
+
// any content.
|
|
14
|
+
//
|
|
15
|
+
// Use this in every new log call that would otherwise want to print
|
|
16
|
+
// a `promptPreview`. Existing call sites should migrate over time.
|
|
17
|
+
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
|
|
20
|
+
const PROMPT_SHA256_PREFIX_CHARS = 12;
|
|
21
|
+
|
|
22
|
+
export interface PromptMeta {
|
|
23
|
+
length: number;
|
|
24
|
+
sha256: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function promptMeta(prompt: string): PromptMeta {
|
|
28
|
+
return {
|
|
29
|
+
length: prompt.length,
|
|
30
|
+
sha256: createHash("sha256").update(prompt).digest("hex").slice(0, PROMPT_SHA256_PREFIX_CHARS),
|
|
31
|
+
};
|
|
32
|
+
}
|
package/server/utils/request.ts
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
// Centralizes patterns that were duplicated across route handlers
|
|
4
4
|
// (3+ different ways to read `req.query.session`).
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// `query: object` so the helpers work with any Express Request
|
|
7
|
+
// generic — `Request<Params, ResBody, ReqBody, Query>`. A narrow
|
|
8
|
+
// `Query` generic like `{ path?: string }` isn't assignable to
|
|
9
|
+
// `Record<string, unknown>` (no index signature), so we widen to
|
|
10
|
+
// `object` and cast internally when reading a key.
|
|
9
11
|
interface HasQuery {
|
|
10
|
-
query:
|
|
12
|
+
query: object;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readQueryKey(queryObj: object, key: string): unknown {
|
|
16
|
+
return (queryObj as Record<string, unknown>)[key];
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
/**
|
|
@@ -15,7 +21,7 @@ interface HasQuery {
|
|
|
15
21
|
* Returns the string value, or "" if missing/non-string.
|
|
16
22
|
*/
|
|
17
23
|
export function getSessionQuery(req: HasQuery): string {
|
|
18
|
-
const raw = req.query
|
|
24
|
+
const raw = readQueryKey(req.query, "session");
|
|
19
25
|
return typeof raw === "string" ? raw : "";
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -24,6 +30,6 @@ export function getSessionQuery(req: HasQuery): string {
|
|
|
24
30
|
* Returns the string value, or undefined if missing/non-string.
|
|
25
31
|
*/
|
|
26
32
|
export function getOptionalStringQuery(req: HasQuery, key: string): string | undefined {
|
|
27
|
-
const raw = req.query
|
|
33
|
+
const raw = readQueryKey(req.query, key);
|
|
28
34
|
return typeof raw === "string" ? raw : undefined;
|
|
29
35
|
}
|
package/server/utils/slug.ts
CHANGED
|
@@ -5,6 +5,19 @@ import { createHash } from "crypto";
|
|
|
5
5
|
// collisions are effectively impossible for any realistic workspace.
|
|
6
6
|
const NON_ASCII_HASH_LEN = 16;
|
|
7
7
|
|
|
8
|
+
// Max slug length used by both `slugify` (output cap) and `isValidSlug`
|
|
9
|
+
// (acceptance cap). 120 leaves plenty of room for filename slugs while
|
|
10
|
+
// staying well under filesystem path limits and URL-segment conventions.
|
|
11
|
+
// Bumped from 64 alongside the slug-rule unification (#732) so journal /
|
|
12
|
+
// todo / wiki / files can all share one rule without truncating their
|
|
13
|
+
// previously-longer inputs.
|
|
14
|
+
//
|
|
15
|
+
// Exported so callers that compose a base slug with their own suffix
|
|
16
|
+
// (e.g. `${base}-2` collision avoidance) can stay inside the same cap
|
|
17
|
+
// — necessary because a 120-char base + "-2" would otherwise produce a
|
|
18
|
+
// 122-char id that fails `isValidSlug`.
|
|
19
|
+
export const DEFAULT_MAX_LENGTH = 120;
|
|
20
|
+
|
|
8
21
|
// eslint-disable-next-line no-control-regex
|
|
9
22
|
const NON_ASCII_RE = /[^\x00-\x7F]/;
|
|
10
23
|
|
|
@@ -18,18 +31,18 @@ export function hashSlug(input: string, length: number = NON_ASCII_HASH_LEN): st
|
|
|
18
31
|
return createHash("sha256").update(input, "utf-8").digest("base64url").slice(0, length);
|
|
19
32
|
}
|
|
20
33
|
|
|
21
|
-
// Validates a slug: lowercase alphanumeric + hyphens, 1–
|
|
22
|
-
// no leading/trailing hyphen, no consecutive hyphens. Previously
|
|
34
|
+
// Validates a slug: lowercase alphanumeric + hyphens, 1–DEFAULT_MAX_LENGTH
|
|
35
|
+
// chars, no leading/trailing hyphen, no consecutive hyphens. Previously
|
|
23
36
|
// duplicated in sources/paths.ts and skills/paths.ts.
|
|
24
37
|
export function isValidSlug(slug: string): boolean {
|
|
25
38
|
if (typeof slug !== "string") return false;
|
|
26
|
-
if (slug.length === 0 || slug.length >
|
|
39
|
+
if (slug.length === 0 || slug.length > DEFAULT_MAX_LENGTH) return false;
|
|
27
40
|
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(slug)) return false;
|
|
28
41
|
if (slug.includes("--")) return false;
|
|
29
42
|
return true;
|
|
30
43
|
}
|
|
31
44
|
|
|
32
|
-
export function slugify(title: string, defaultSlug = "page", maxLength =
|
|
45
|
+
export function slugify(title: string, defaultSlug = "page", maxLength = DEFAULT_MAX_LENGTH): string {
|
|
33
46
|
const asciiSlug = title
|
|
34
47
|
.toLowerCase()
|
|
35
48
|
.replace(/[^a-z0-9]+/g, "-")
|
|
@@ -48,3 +61,51 @@ export function slugify(title: string, defaultSlug = "page", maxLength = 60): st
|
|
|
48
61
|
}
|
|
49
62
|
return hash;
|
|
50
63
|
}
|
|
64
|
+
|
|
65
|
+
// Disambiguate `base` against `existingIds` by appending `-2`, `-3`, …
|
|
66
|
+
// until a free id is found. Truncates `base` (stripping any trailing
|
|
67
|
+
// hyphen so the join never produces `--`) so the composite stays
|
|
68
|
+
// within `DEFAULT_MAX_LENGTH` — required because a 120-char base + "-2"
|
|
69
|
+
// would otherwise produce a 122-char id that fails `isValidSlug`.
|
|
70
|
+
//
|
|
71
|
+
// Shared between server (`todosColumnsHandlers#uniqueId`) and the
|
|
72
|
+
// Playwright mock (`e2e/tests/todo-columns.spec.ts`) so the e2e
|
|
73
|
+
// dispatcher mirrors server behavior at the truncation boundary.
|
|
74
|
+
//
|
|
75
|
+
// Precondition: `base` must already be a canonical slug — typically
|
|
76
|
+
// the output of `slugify`. Concretely: lowercase ASCII alnum +
|
|
77
|
+
// hyphens, no leading/trailing hyphen, no `--`, length within
|
|
78
|
+
// `DEFAULT_MAX_LENGTH`. Inputs outside that grammar (e.g. uppercase,
|
|
79
|
+
// punctuation, leading hyphen) get NO defensive treatment — if they
|
|
80
|
+
// collide they will be disambiguated to an equally-invalid suffix
|
|
81
|
+
// form. The helper only special-cases empty/all-hyphen bases (where
|
|
82
|
+
// no valid disambiguation exists at all) and returns them
|
|
83
|
+
// unchanged so it never *fabricates* a `-2`-style invalid slug from
|
|
84
|
+
// nothing. Codex iter-4/5 #732.
|
|
85
|
+
export function disambiguateSlug(base: string, existingIds: ReadonlySet<string>): string {
|
|
86
|
+
if (!hasNonHyphenChar(base)) return base;
|
|
87
|
+
if (!existingIds.has(base)) return base;
|
|
88
|
+
const compose = (suffix: number): string => {
|
|
89
|
+
const tail = `-${suffix}`;
|
|
90
|
+
const room = DEFAULT_MAX_LENGTH - tail.length;
|
|
91
|
+
// Pick the cut: full base when it already fits, room-truncated
|
|
92
|
+
// otherwise. Then strip any trailing hyphen at that cut so the
|
|
93
|
+
// join never yields `--` (which `isValidSlug` rejects). Manual
|
|
94
|
+
// loop instead of a regex — `sonarjs/slow-regex` flags `-+$`
|
|
95
|
+
// even on this bounded input.
|
|
96
|
+
const cut = base.length <= room ? base.length : room;
|
|
97
|
+
let end = cut;
|
|
98
|
+
while (end > 0 && base[end - 1] === "-") end--;
|
|
99
|
+
return `${base.slice(0, end)}${tail}`;
|
|
100
|
+
};
|
|
101
|
+
let suffix = 2;
|
|
102
|
+
while (existingIds.has(compose(suffix))) suffix++;
|
|
103
|
+
return compose(suffix);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hasNonHyphenChar(input: string): boolean {
|
|
107
|
+
for (let i = 0; i < input.length; i++) {
|
|
108
|
+
if (input[i] !== "-") return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
package/server/utils/spawn.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function extractClaudeErrorMessage(stdout: string): string | null {
|
|
|
28
28
|
if (!isRecord(parsed)) return null;
|
|
29
29
|
if (parsed.is_error !== true) return null;
|
|
30
30
|
if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
|
|
31
|
-
const joined = parsed.errors.filter((
|
|
31
|
+
const joined = parsed.errors.filter((err): err is string => typeof err === "string").join("; ");
|
|
32
32
|
if (joined.length > 0) return joined;
|
|
33
33
|
}
|
|
34
34
|
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
|
package/server/utils/types.ts
CHANGED
|
@@ -23,12 +23,12 @@ export function isNonEmptyString(value: unknown): value is string {
|
|
|
23
23
|
/** Record whose values are all strings. */
|
|
24
24
|
export function isStringRecord(value: unknown): value is Record<string, string> {
|
|
25
25
|
if (!isRecord(value)) return false;
|
|
26
|
-
return Object.values(value).every((
|
|
26
|
+
return Object.values(value).every((val) => typeof val === "string");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** String array (every element is a string). */
|
|
30
30
|
export function isStringArray(value: unknown): value is string[] {
|
|
31
|
-
return Array.isArray(value) && value.every((
|
|
31
|
+
return Array.isArray(value) && value.every((val) => typeof val === "string");
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** Error-like object with a `code` property (e.g. Node.js fs errors). */
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// point at a `mkdtempSync` directory.
|
|
15
15
|
|
|
16
16
|
import { workspacePath as defaultWorkspacePath } from "../workspace.js";
|
|
17
|
-
import { ClaudeCliNotFoundError } from "../journal/archivist.js";
|
|
17
|
+
import { ClaudeCliNotFoundError } from "../journal/archivist-cli.js";
|
|
18
18
|
import { indexSession, listSessionIds, type IndexerDeps } from "./indexer.js";
|
|
19
19
|
import { log } from "../../system/logger/index.js";
|
|
20
20
|
|
|
@@ -17,7 +17,7 @@ import { EVENT_TYPES } from "../../../src/types/events.js";
|
|
|
17
17
|
import { readFile } from "node:fs/promises";
|
|
18
18
|
import { formatSpawnFailure } from "../../utils/spawn.js";
|
|
19
19
|
import { tmpdir } from "node:os";
|
|
20
|
-
import { ClaudeCliNotFoundError } from "../journal/archivist.js";
|
|
20
|
+
import { ClaudeCliNotFoundError } from "../journal/archivist-cli.js";
|
|
21
21
|
import { errorMessage } from "../../utils/errors.js";
|
|
22
22
|
import type { SummaryResult } from "./types.js";
|
|
23
23
|
import { ONE_MINUTE_MS } from "../../utils/time.js";
|
|
@@ -152,10 +152,10 @@ export function validateSummaryResult(obj: unknown): SummaryResult {
|
|
|
152
152
|
if (!isRecord(obj)) {
|
|
153
153
|
throw new Error("[chat-index] summary result is not an object");
|
|
154
154
|
}
|
|
155
|
-
const
|
|
156
|
-
const title = typeof
|
|
157
|
-
const summary = typeof
|
|
158
|
-
const keywords = Array.isArray(
|
|
155
|
+
const record = obj as Record<string, unknown>;
|
|
156
|
+
const title = typeof record.title === "string" ? record.title : "";
|
|
157
|
+
const summary = typeof record.summary === "string" ? record.summary : "";
|
|
158
|
+
const keywords = Array.isArray(record.keywords) ? record.keywords.filter((keyword): keyword is string => typeof keyword === "string") : [];
|
|
159
159
|
return { title, summary, keywords };
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// directories under `data/` and `artifacts/` for organizing files.
|
|
5
5
|
// Claude sees these in the system prompt and routes saves accordingly.
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { workspacePath, WORKSPACE_DIRS } from "./paths.js";
|
|
10
10
|
import { log } from "../system/logger/index.js";
|
|
@@ -102,8 +102,8 @@ export function loadCustomDirs(root?: string): CustomDirEntry[] {
|
|
|
102
102
|
const base = root ?? workspacePath;
|
|
103
103
|
const filePath = path.join(base, CONFIG_FILE);
|
|
104
104
|
try {
|
|
105
|
-
if (!
|
|
106
|
-
const raw =
|
|
105
|
+
if (!existsSync(filePath)) return [];
|
|
106
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
107
107
|
const parsed: unknown = JSON.parse(raw);
|
|
108
108
|
if (!Array.isArray(parsed)) {
|
|
109
109
|
log.warn("custom-dirs", "workspace-dirs.json is not an array");
|
|
@@ -132,7 +132,7 @@ export function loadCustomDirs(root?: string): CustomDirEntry[] {
|
|
|
132
132
|
export function saveCustomDirs(entries: readonly CustomDirEntry[], root?: string): void {
|
|
133
133
|
const base = root ?? workspacePath;
|
|
134
134
|
const filePath = path.join(base, CONFIG_FILE);
|
|
135
|
-
|
|
135
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
136
136
|
writeFileAtomicSync(filePath, JSON.stringify(entries, null, 2));
|
|
137
137
|
invalidateCache();
|
|
138
138
|
}
|
|
@@ -186,7 +186,7 @@ export function ensureCustomDirs(entries: readonly CustomDirEntry[], root?: stri
|
|
|
186
186
|
const base = root ?? workspacePath;
|
|
187
187
|
for (const entry of entries) {
|
|
188
188
|
const dirPath = path.join(base, entry.path);
|
|
189
|
-
|
|
189
|
+
mkdirSync(dirPath, { recursive: true });
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Gemini API Key
|
|
2
|
+
|
|
3
|
+
MulmoClaude uses Google's Gemini API for image, audio, and video generation. Setting the `GEMINI_API_KEY` environment variable is **technically optional**, but we **strongly recommend** it — a large portion of MulmoClaude's visual and multimedia output depends on it.
|
|
4
|
+
|
|
5
|
+
A single key unlocks all three capabilities (images, TTS audio, video) — you don't need separate credentials for each.
|
|
6
|
+
|
|
7
|
+
## What the Key Unlocks
|
|
8
|
+
|
|
9
|
+
### Images
|
|
10
|
+
|
|
11
|
+
- **`generateImage`** — creates images from text prompts. The heart of the **Artist** role.
|
|
12
|
+
- **`editImage`** — transforms or restyles an existing image ("convert to Ghibli style", "remove the background", "add a sunset"). Also **Artist**.
|
|
13
|
+
- **Inline document images** — roles that produce rich documents (**Guide & Planner**, **Tutor**, Recipe Guide, Trip Planner, …) embed generated images directly into the page via `presentDocument`. Without a key, those image slots fall back to italic "🖼️ Image: <prompt>" text markers — the prompt is preserved, but no picture renders.
|
|
14
|
+
- **MulmoScript image beats** — `presentMulmoScript` uses Gemini image models for `imagePrompt` beats. **Storyteller Plus** additionally uses them for consistent-character scenes across a storyboard.
|
|
15
|
+
|
|
16
|
+
### Audio
|
|
17
|
+
|
|
18
|
+
- **MulmoScript speech** — `presentMulmoScript` synthesizes speaker voices via Gemini TTS (`gemini-2.5-flash-preview-tts`). This is what turns a storyboard into spoken narration, so the **Storyteller** and **Storyteller Plus** roles become near-complete multimedia pieces.
|
|
19
|
+
|
|
20
|
+
### Video
|
|
21
|
+
|
|
22
|
+
- **MulmoScript movie beats** — beats whose `image.type` is `moviePrompt` are rendered with Google's **Veo** models (`veo-2.0-generate-001`, `veo-3.0-generate-001`, …). Without a key, movie beats can't be produced.
|
|
23
|
+
|
|
24
|
+
### Bottom line
|
|
25
|
+
|
|
26
|
+
Without a Gemini API key:
|
|
27
|
+
|
|
28
|
+
- The **Artist** role has nothing to generate.
|
|
29
|
+
- **Storyteller** and **Storyteller Plus** still produce a storyboard, but without images or narration.
|
|
30
|
+
- Rich documents from **Guide & Planner**, **Tutor**, and similar roles render as text-only with placeholder markers where images should be.
|
|
31
|
+
|
|
32
|
+
## How to Get a Key
|
|
33
|
+
|
|
34
|
+
The Gemini API has a **free tier that is sufficient for personal use**. Higher-volume or premium-model use (e.g. Veo video) may require a paid plan.
|
|
35
|
+
|
|
36
|
+
1. Open [Google AI Studio → API keys](https://aistudio.google.com/apikey) and sign in with a Google account.
|
|
37
|
+
2. Click **Create API key**. If prompted, select or create a Google Cloud project (any project will do).
|
|
38
|
+
3. Copy the key — it starts with `AIza…`.
|
|
39
|
+
4. Open the project's `.env` file in the repository root (copy `.env.example` first if it doesn't exist yet) and add the line:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
GEMINI_API_KEY=AIza…your-key…
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
5. Restart MulmoClaude so the new environment variable is picked up.
|
|
46
|
+
|
|
47
|
+
## Verifying It's Active
|
|
48
|
+
|
|
49
|
+
The quickest check: switch to the **Artist** role and ask for _"an image of a red panda"_. If a real image appears in the canvas (instead of an italic text marker or a disabled-role hint), the key is wired up correctly.
|
|
50
|
+
|
|
51
|
+
You can also inspect the server log on startup: messages like `GEMINI_API_KEY not set — image placeholders will render as text markers` indicate the key is missing or misread.
|
|
52
|
+
|
|
53
|
+
## Security
|
|
54
|
+
|
|
55
|
+
- The key lives in your local `.env` file. MulmoClaude never uploads it to its own servers or to Anthropic — requests go directly from your machine to Google.
|
|
56
|
+
- Treat the key like a password. Anyone who sees it can make billable API calls against your Google account.
|
|
57
|
+
- If you suspect a key has leaked, revoke it from [Google AI Studio → API keys](https://aistudio.google.com/apikey) and generate a new one.
|
|
@@ -15,7 +15,6 @@ Under the hood it uses the Claude Code Agent SDK as its LLM core. Claude has ful
|
|
|
15
15
|
- **Tutor** — Assesses your knowledge level, then teaches any topic with structured documents and visuals.
|
|
16
16
|
- **Storyteller** — Crafts illustrated narrative stories as a MulmoScript storyboard.
|
|
17
17
|
- **Storyteller Plus** — Like Storyteller, with consistent character images across beats.
|
|
18
|
-
- **Role Manager** — Create, edit, and delete custom roles.
|
|
19
18
|
- *(Additional roles may be defined by the user in the workspace.)*
|
|
20
19
|
|
|
21
20
|
## Key Capabilities
|
|
@@ -40,11 +39,13 @@ See [Wiki](helps/wiki.md) for details on how it works.
|
|
|
40
39
|
## Help Pages
|
|
41
40
|
|
|
42
41
|
- [Wiki](helps/wiki.md) — how the personal knowledge wiki works, its folder layout, page format, and operations
|
|
42
|
+
- [Gemini API Key](helps/gemini.md) — why `GEMINI_API_KEY` is strongly recommended (images, audio, video) and how to get one from Google AI Studio
|
|
43
43
|
- [MulmoScript](helps/mulmoscript.md) — format reference for authoring multimedia stories: beats, image types, speech, audio, and a minimal example
|
|
44
44
|
- [Business Presentation Template](helps/business.md) — MulmoScript template and rules for business presentations in the Office role
|
|
45
45
|
- [Spreadsheet](helps/spreadsheet.md) — cell format, formulas, date handling, and format codes for the presentSpreadsheet plugin
|
|
46
46
|
- [Sandbox](helps/sandbox.md) — how the Docker sandbox isolates the agent, what it can access, and how to disable it
|
|
47
47
|
- [Telegram Bridge](helps/telegram.md) — how to talk to MulmoClaude from the Telegram app: creating a bot, starting the bridge, allowlisting chat IDs, commands, and troubleshooting
|
|
48
|
+
- [Information Sources](helps/sources.md) — registering RSS feeds / GitHub repos / arXiv queries, the daily-brief pipeline, and where its files live on disk
|
|
48
49
|
|
|
49
50
|
## Workspace Layout
|
|
50
51
|
|