mulmoclaude 0.1.2 → 0.4.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-Cv5snK9D.js +5 -0
- package/client/assets/index-CubzmCVK.css +2 -0
- package/client/assets/{index-D8rhwXLq.js → index-DtcyExH9.js} +80 -61
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
- package/client/index.html +2 -4
- package/package.json +13 -13
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/config.ts +12 -12
- package/server/agent/index.ts +9 -3
- package/server/agent/mcp-server.ts +19 -19
- package/server/agent/mcp-tools/index.ts +6 -6
- package/server/agent/mcp-tools/x.ts +7 -6
- package/server/agent/prompt.ts +195 -29
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +10 -10
- package/server/agent/stream.ts +4 -4
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +21 -3
- package/server/api/routes/config.ts +1 -1
- package/server/api/routes/files.ts +22 -21
- package/server/api/routes/html.ts +2 -2
- package/server/api/routes/image.ts +7 -7
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +16 -6
- package/server/api/routes/roles.ts +2 -2
- package/server/api/routes/scheduler.ts +14 -12
- package/server/api/routes/schedulerHandlers.ts +12 -12
- package/server/api/routes/schedulerTasks.ts +19 -17
- package/server/api/routes/sessions.ts +26 -26
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +5 -5
- package/server/api/routes/sources.ts +3 -3
- package/server/api/routes/todosColumnsHandlers.ts +30 -30
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki.ts +36 -22
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +6 -6
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +17 -16
- package/server/events/scheduler-adapter.ts +20 -20
- package/server/events/session-store/index.ts +10 -10
- package/server/events/task-manager/index.ts +7 -7
- package/server/index.ts +59 -65
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +5 -5
- package/server/utils/date.ts +18 -18
- package/server/utils/files/atomic.ts +16 -16
- package/server/utils/files/html-io.ts +5 -5
- package/server/utils/files/image-store.ts +19 -8
- package/server/utils/files/journal-io.ts +4 -4
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-store.ts +4 -4
- package/server/utils/files/naming.ts +2 -2
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +12 -12
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-io.ts +5 -5
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/session-io.ts +35 -35
- package/server/utils/files/spreadsheet-store.ts +7 -7
- package/server/utils/files/todos-io.ts +9 -9
- package/server/utils/files/user-tasks-io.ts +5 -5
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +2 -2
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- 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/request.ts +12 -6
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/indexer.ts +15 -15
- package/server/workspace/chat-index/summarizer.ts +4 -4
- package/server/workspace/custom-dirs.ts +16 -16
- package/server/workspace/journal/archivist.ts +35 -35
- package/server/workspace/journal/dailyPass.ts +31 -28
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +4 -4
- package/server/workspace/journal/indexFile.ts +29 -25
- package/server/workspace/journal/optimizationPass.ts +2 -2
- package/server/workspace/journal/state.ts +6 -6
- package/server/workspace/paths.ts +3 -3
- package/server/workspace/reference-dirs.ts +20 -20
- package/server/workspace/roles.ts +6 -6
- package/server/workspace/skills/discovery.ts +4 -4
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +3 -3
- package/server/workspace/skills/user-tasks.ts +34 -34
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +10 -10
- package/server/workspace/sources/classifier.ts +7 -7
- package/server/workspace/sources/fetchers/arxiv.ts +7 -7
- package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
- package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +12 -12
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +36 -13
- package/server/workspace/sources/pipeline/index.ts +8 -13
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/plan.ts +15 -13
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/registry.ts +16 -16
- package/server/workspace/sources/robots.ts +14 -14
- package/server/workspace/sources/sourceState.ts +17 -10
- 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/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +26 -16
- package/server/workspace/wiki-backlinks/index.ts +8 -8
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +315 -141
- package/src/components/CanvasViewToggle.vue +10 -7
- package/src/components/ChatInput.vue +67 -33
- package/src/components/FileContentHeader.vue +7 -4
- package/src/components/FileContentRenderer.vue +20 -6
- package/src/components/FileTree.vue +6 -3
- package/src/components/FileTreePane.vue +11 -8
- package/src/components/FilesView.vue +5 -3
- package/src/components/LockStatusPopup.vue +17 -14
- package/src/components/NotificationBell.vue +14 -5
- package/src/components/NotificationToast.vue +6 -3
- package/src/components/PluginLauncher.vue +19 -56
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +2 -2
- package/src/components/SessionHistoryPanel.vue +38 -34
- package/src/components/SessionTabBar.vue +8 -10
- package/src/components/SettingsMcpTab.vue +49 -36
- package/src/components/SettingsModal.vue +24 -22
- package/src/components/SettingsReferenceDirsTab.vue +39 -34
- package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
- package/src/components/SidebarHeader.vue +25 -4
- package/src/components/StackView.vue +4 -1
- package/src/components/SuggestionsPanel.vue +7 -4
- package/src/components/TodoExplorer.vue +26 -15
- package/src/components/ToolResultsPanel.vue +27 -13
- package/src/components/todo/TodoAddDialog.vue +19 -14
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +17 -12
- package/src/components/todo/TodoKanbanView.vue +10 -5
- package/src/components/todo/TodoListView.vue +10 -7
- package/src/components/todo/TodoTableView.vue +5 -2
- package/src/composables/useAppApi.ts +9 -0
- package/src/composables/useClickOutside.ts +2 -2
- package/src/composables/useDynamicFavicon.ts +172 -37
- package/src/composables/useEventListeners.ts +7 -8
- package/src/composables/useFaviconState.ts +13 -2
- package/src/composables/useFileSelection.ts +24 -6
- package/src/composables/useFreshPluginData.ts +3 -3
- package/src/composables/useKeyNavigation.ts +11 -11
- package/src/composables/useLayoutMode.ts +32 -0
- package/src/composables/useMcpTools.ts +2 -2
- package/src/composables/useNotifications.ts +3 -3
- package/src/composables/usePdfDownload.ts +4 -4
- package/src/composables/usePendingCalls.ts +1 -1
- package/src/composables/usePubSub.ts +10 -10
- package/src/composables/useRoles.ts +1 -1
- package/src/composables/useSandboxStatus.ts +1 -1
- package/src/composables/useSessionDerived.ts +3 -3
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSessionSync.ts +8 -8
- package/src/composables/useViewLayout.ts +20 -34
- package/src/config/roles.ts +2 -2
- package/src/lang/de.ts +536 -0
- package/src/lang/en.ts +558 -0
- package/src/lang/es.ts +543 -0
- package/src/lang/fr.ts +536 -0
- package/src/lang/ja.ts +536 -0
- package/src/lang/ko.ts +540 -0
- package/src/lang/pt-BR.ts +534 -0
- package/src/lang/zh.ts +537 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +2 -0
- package/src/plugins/canvas/View.vue +102 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/chart/Preview.vue +5 -5
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +59 -43
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +29 -25
- package/src/plugins/manageSource/Preview.vue +2 -2
- package/src/plugins/manageSource/View.vue +73 -52
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +26 -36
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/helpers.ts +8 -8
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +40 -30
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/scheduler/Preview.vue +13 -10
- package/src/plugins/scheduler/TasksTab.vue +57 -28
- package/src/plugins/scheduler/View.vue +28 -19
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/spreadsheet/Preview.vue +8 -3
- package/src/plugins/spreadsheet/View.vue +21 -12
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +29 -9
- package/src/plugins/todo/Preview.vue +13 -8
- package/src/plugins/todo/View.vue +38 -24
- package/src/plugins/todo/composables/useTodos.ts +5 -5
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +10 -7
- package/src/plugins/wiki/View.vue +202 -81
- package/src/plugins/wiki/helpers.ts +4 -4
- package/src/plugins/wiki/route.ts +112 -0
- package/src/router/guards.ts +46 -28
- package/src/router/index.ts +41 -26
- package/src/types/session.ts +4 -3
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/request.ts +22 -3
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/dom/scrollable.ts +2 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/files/sortChildren.ts +6 -6
- package/src/utils/format/frontmatter.ts +6 -6
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
- package/src/utils/markdown/extractFirstH1.ts +2 -2
- package/src/utils/path/relativeLink.ts +15 -15
- package/src/utils/path/workspaceLinkRouter.ts +81 -0
- package/src/utils/role/icon.ts +2 -2
- package/src/utils/role/merge.ts +2 -2
- package/src/utils/role/plugins.ts +1 -1
- package/src/utils/session/sessionFactory.ts +2 -2
- package/src/utils/session/sessionHelpers.ts +2 -2
- package/src/utils/tools/dedup.ts +4 -4
- package/src/utils/tools/result.ts +3 -3
- package/src/utils/types.ts +2 -2
- 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-KNLBjwuh.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/utils/canvas/viewMode.ts +0 -46
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
|
@@ -18,12 +18,12 @@ export function ensureChatDir(): void {
|
|
|
18
18
|
ensureWorkspaceDir(CHAT);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function metaRel(
|
|
22
|
-
return path.posix.join(CHAT, `${
|
|
21
|
+
function metaRel(sessionId: string): string {
|
|
22
|
+
return path.posix.join(CHAT, `${sessionId}.json`);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function jsonlRel(
|
|
26
|
-
return path.posix.join(CHAT, `${
|
|
25
|
+
function jsonlRel(sessionId: string): string {
|
|
26
|
+
return path.posix.join(CHAT, `${sessionId}.jsonl`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// ── Meta ────────────────────────────────────────────────────────
|
|
@@ -41,8 +41,8 @@ export interface SessionMeta {
|
|
|
41
41
|
export type ReadMetaResult = { kind: "missing" } | { kind: "ok"; meta: SessionMeta } | { kind: "corrupt"; raw: string };
|
|
42
42
|
|
|
43
43
|
/** Read session metadata with full outcome discrimination. */
|
|
44
|
-
export async function readSessionMetaFull(
|
|
45
|
-
const raw = await readTextUnder(root(rootOverride), metaRel(
|
|
44
|
+
export async function readSessionMetaFull(sessionId: string, rootOverride?: string): Promise<ReadMetaResult> {
|
|
45
|
+
const raw = await readTextUnder(root(rootOverride), metaRel(sessionId));
|
|
46
46
|
if (raw === null) return { kind: "missing" };
|
|
47
47
|
try {
|
|
48
48
|
return { kind: "ok", meta: JSON.parse(raw) as SessionMeta };
|
|
@@ -53,60 +53,60 @@ export async function readSessionMetaFull(id: string, rootOverride?: string): Pr
|
|
|
53
53
|
|
|
54
54
|
/** Convenience: returns the meta or null. Treats corrupt as null
|
|
55
55
|
* (callers that need to distinguish use readSessionMetaFull). */
|
|
56
|
-
export async function readSessionMeta(
|
|
57
|
-
const result = await readSessionMetaFull(
|
|
56
|
+
export async function readSessionMeta(sessionId: string, rootOverride?: string): Promise<SessionMeta | null> {
|
|
57
|
+
const result = await readSessionMetaFull(sessionId, rootOverride);
|
|
58
58
|
return result.kind === "ok" ? result.meta : null;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export async function writeSessionMeta(
|
|
62
|
-
await writeTextUnder(root(rootOverride), metaRel(
|
|
61
|
+
export async function writeSessionMeta(sessionId: string, meta: SessionMeta, rootOverride?: string): Promise<void> {
|
|
62
|
+
await writeTextUnder(root(rootOverride), metaRel(sessionId), JSON.stringify(meta, null, 2));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export async function createSessionMeta(
|
|
65
|
+
export async function createSessionMeta(sessionId: string, roleId: string, firstUserMessage: string, rootOverride?: string, origin?: string): Promise<void> {
|
|
66
66
|
const meta: Record<string, unknown> = {
|
|
67
67
|
roleId,
|
|
68
68
|
startedAt: new Date().toISOString(),
|
|
69
69
|
firstUserMessage,
|
|
70
70
|
};
|
|
71
71
|
if (origin) meta.origin = origin;
|
|
72
|
-
await writeSessionMeta(
|
|
72
|
+
await writeSessionMeta(sessionId, meta, rootOverride);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export async function backfillOrigin(
|
|
76
|
-
const meta = await readSessionMeta(
|
|
75
|
+
export async function backfillOrigin(sessionId: string, origin: SessionMeta["origin"], rootOverride?: string): Promise<void> {
|
|
76
|
+
const meta = await readSessionMeta(sessionId, rootOverride);
|
|
77
77
|
if (!meta || meta.origin) return; // already set
|
|
78
|
-
await writeSessionMeta(
|
|
78
|
+
await writeSessionMeta(sessionId, { ...meta, origin }, rootOverride);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
export async function backfillFirstUserMessage(
|
|
82
|
-
const meta = await readSessionMeta(
|
|
81
|
+
export async function backfillFirstUserMessage(sessionId: string, message: string, rootOverride?: string): Promise<void> {
|
|
82
|
+
const meta = await readSessionMeta(sessionId, rootOverride);
|
|
83
83
|
if (!meta || meta.firstUserMessage) return;
|
|
84
|
-
await writeSessionMeta(
|
|
84
|
+
await writeSessionMeta(sessionId, { ...meta, firstUserMessage: message }, rootOverride);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export async function setClaudeSessionId(
|
|
88
|
-
const meta = await readSessionMeta(
|
|
87
|
+
export async function setClaudeSessionId(sessionId: string, claudeSessionId: string, rootOverride?: string): Promise<void> {
|
|
88
|
+
const meta = await readSessionMeta(sessionId, rootOverride);
|
|
89
89
|
if (!meta) return;
|
|
90
|
-
await writeSessionMeta(
|
|
90
|
+
await writeSessionMeta(sessionId, { ...meta, claudeSessionId }, rootOverride);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export async function clearClaudeSessionId(
|
|
94
|
-
const meta = await readSessionMeta(
|
|
93
|
+
export async function clearClaudeSessionId(sessionId: string, rootOverride?: string): Promise<void> {
|
|
94
|
+
const meta = await readSessionMeta(sessionId, rootOverride);
|
|
95
95
|
if (!meta) return;
|
|
96
96
|
const { claudeSessionId: __removed, ...rest } = meta;
|
|
97
|
-
await writeSessionMeta(
|
|
97
|
+
await writeSessionMeta(sessionId, rest, rootOverride);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
export async function updateHasUnread(
|
|
101
|
-
const meta = await readSessionMeta(
|
|
100
|
+
export async function updateHasUnread(sessionId: string, hasUnread: boolean, rootOverride?: string): Promise<void> {
|
|
101
|
+
const meta = await readSessionMeta(sessionId, rootOverride);
|
|
102
102
|
if (!meta) return;
|
|
103
|
-
await writeSessionMeta(
|
|
103
|
+
await writeSessionMeta(sessionId, { ...meta, hasUnread }, rootOverride);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// ── Jsonl ───────────────────────────────────────────────────────
|
|
107
107
|
|
|
108
|
-
export function sessionJsonlAbsPath(
|
|
109
|
-
return resolvePath(root(rootOverride), jsonlRel(
|
|
108
|
+
export function sessionJsonlAbsPath(sessionId: string, rootOverride?: string): string {
|
|
109
|
+
return resolvePath(root(rootOverride), jsonlRel(sessionId));
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
@@ -115,12 +115,12 @@ export function sessionJsonlAbsPath(id: string, rootOverride?: string): string {
|
|
|
115
115
|
* `hasUnread`, `roleId`, `startedAt`, `origin`, etc. Its mtime bumps
|
|
116
116
|
* whenever any of those fields change via `writeSessionMeta`.
|
|
117
117
|
*/
|
|
118
|
-
export function sessionMetaAbsPath(
|
|
119
|
-
return resolvePath(root(rootOverride), metaRel(
|
|
118
|
+
export function sessionMetaAbsPath(sessionId: string, rootOverride?: string): string {
|
|
119
|
+
return resolvePath(root(rootOverride), metaRel(sessionId));
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
export async function readSessionJsonl(
|
|
123
|
-
return readTextUnder(root(rootOverride), jsonlRel(
|
|
122
|
+
export async function readSessionJsonl(sessionId: string, rootOverride?: string): Promise<string | null> {
|
|
123
|
+
return readTextUnder(root(rootOverride), jsonlRel(sessionId));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|
|
@@ -130,7 +130,7 @@ export async function readSessionJsonl(id: string, rootOverride?: string): Promi
|
|
|
130
130
|
* content and don't need to worry about line termination. This
|
|
131
131
|
* prevents JSONL parse failures from missing newlines.
|
|
132
132
|
*/
|
|
133
|
-
export async function appendSessionLine(
|
|
133
|
+
export async function appendSessionLine(sessionId: string, line: string, rootOverride?: string): Promise<void> {
|
|
134
134
|
const normalized = line.endsWith("\n") ? line : `${line}\n`;
|
|
135
|
-
await appendFile(resolvePath(root(rootOverride), jsonlRel(
|
|
135
|
+
await appendFile(resolvePath(root(rootOverride), jsonlRel(sessionId)), normalized);
|
|
136
136
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { mkdir, realpath, writeFile } from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import crypto from "crypto";
|
|
4
4
|
import { WORKSPACE_DIRS, WORKSPACE_PATHS } from "../../workspace/paths.js";
|
|
@@ -13,8 +13,8 @@ let spreadsheetsDirReal: string | null = null;
|
|
|
13
13
|
|
|
14
14
|
async function ensureSpreadsheetsDir(): Promise<string> {
|
|
15
15
|
if (spreadsheetsDirReal) return spreadsheetsDirReal;
|
|
16
|
-
await
|
|
17
|
-
spreadsheetsDirReal = await
|
|
16
|
+
await mkdir(SPREADSHEETS_DIR, { recursive: true });
|
|
17
|
+
spreadsheetsDirReal = await realpath(SPREADSHEETS_DIR);
|
|
18
18
|
return spreadsheetsDirReal;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -36,16 +36,16 @@ async function safeResolve(relativePath: string): Promise<string> {
|
|
|
36
36
|
/** Save sheets array as a JSON file. Returns the workspace-relative path. */
|
|
37
37
|
export async function saveSpreadsheet(sheets: unknown[]): Promise<string> {
|
|
38
38
|
await ensureSpreadsheetsDir();
|
|
39
|
-
const
|
|
40
|
-
const filename = `${
|
|
41
|
-
await
|
|
39
|
+
const sheetId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
40
|
+
const filename = `${sheetId}.json`;
|
|
41
|
+
await writeFile(path.join(SPREADSHEETS_DIR, filename), JSON.stringify(sheets), "utf-8");
|
|
42
42
|
return path.posix.join(WORKSPACE_DIRS.spreadsheets, filename);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** Overwrite an existing spreadsheet file. */
|
|
46
46
|
export async function overwriteSpreadsheet(relativePath: string, sheets: unknown[]): Promise<void> {
|
|
47
47
|
const absPath = await safeResolve(relativePath);
|
|
48
|
-
await
|
|
48
|
+
await writeFile(absPath, JSON.stringify(sheets), "utf-8");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/** Check if a string is a spreadsheet file path (not inline data).
|
|
@@ -10,20 +10,20 @@ import { resolvePath } from "./workspace-io.js";
|
|
|
10
10
|
import { loadJsonFile } from "./json.js";
|
|
11
11
|
import { writeFileAtomicSync } from "./atomic.js";
|
|
12
12
|
|
|
13
|
-
const root = (
|
|
13
|
+
const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
|
|
14
14
|
|
|
15
|
-
export function loadTodos<T>(fallback: T,
|
|
16
|
-
return loadJsonFile(resolvePath(root(
|
|
15
|
+
export function loadTodos<T>(fallback: T, workspaceRoot?: string): T {
|
|
16
|
+
return loadJsonFile(resolvePath(root(workspaceRoot), WORKSPACE_FILES.todosItems), fallback);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function saveTodos(items: unknown,
|
|
20
|
-
writeFileAtomicSync(resolvePath(root(
|
|
19
|
+
export function saveTodos(items: unknown, workspaceRoot?: string): void {
|
|
20
|
+
writeFileAtomicSync(resolvePath(root(workspaceRoot), WORKSPACE_FILES.todosItems), JSON.stringify(items, null, 2));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function loadColumns<T>(fallback: T,
|
|
24
|
-
return loadJsonFile(resolvePath(root(
|
|
23
|
+
export function loadColumns<T>(fallback: T, workspaceRoot?: string): T {
|
|
24
|
+
return loadJsonFile(resolvePath(root(workspaceRoot), WORKSPACE_FILES.todosColumns), fallback);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function saveColumns(columns: unknown,
|
|
28
|
-
writeFileAtomicSync(resolvePath(root(
|
|
27
|
+
export function saveColumns(columns: unknown, workspaceRoot?: string): void {
|
|
28
|
+
writeFileAtomicSync(resolvePath(root(workspaceRoot), WORKSPACE_FILES.todosColumns), JSON.stringify(columns, null, 2));
|
|
29
29
|
}
|
|
@@ -11,15 +11,15 @@ import { resolvePath } from "./workspace-io.js";
|
|
|
11
11
|
import { loadJsonFile } from "./json.js";
|
|
12
12
|
import { writeFileAtomic } from "./atomic.js";
|
|
13
13
|
|
|
14
|
-
const root = (
|
|
14
|
+
const root = (workspaceRoot?: string) => workspaceRoot ?? workspacePath;
|
|
15
15
|
|
|
16
|
-
export function loadUserTasks<T>(
|
|
17
|
-
const tasks = loadJsonFile<T[]>(resolvePath(root(
|
|
16
|
+
export function loadUserTasks<T>(workspaceRoot?: string): T[] {
|
|
17
|
+
const tasks = loadJsonFile<T[]>(resolvePath(root(workspaceRoot), WORKSPACE_FILES.schedulerUserTasks), []);
|
|
18
18
|
return Array.isArray(tasks) ? tasks : [];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export async function saveUserTasks<T>(tasks: T[],
|
|
22
|
-
const filePath = resolvePath(root(
|
|
21
|
+
export async function saveUserTasks<T>(tasks: T[], workspaceRoot?: string): Promise<void> {
|
|
22
|
+
const filePath = resolvePath(root(workspaceRoot), WORKSPACE_FILES.schedulerUserTasks);
|
|
23
23
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
24
24
|
await writeFileAtomic(filePath, JSON.stringify(tasks, null, 2));
|
|
25
25
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// All reads swallow ENOENT and return null / fallback so callers can
|
|
12
12
|
// do `if (!content)` instead of try/catch.
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import { Stats, mkdirSync, promises, readFileSync, readdirSync, statSync } from "fs";
|
|
15
15
|
import path from "path";
|
|
16
16
|
import { workspacePath } from "../../workspace/paths.js";
|
|
17
17
|
import { writeFileAtomic, writeFileAtomicSync } from "./atomic.js";
|
|
@@ -44,7 +44,7 @@ export function resolveWorkspacePath(relPath: string): string {
|
|
|
44
44
|
*/
|
|
45
45
|
export async function readWorkspaceText(relPath: string): Promise<string | null> {
|
|
46
46
|
try {
|
|
47
|
-
return await
|
|
47
|
+
return await promises.readFile(resolveWorkspacePath(relPath), "utf-8");
|
|
48
48
|
} catch (err) {
|
|
49
49
|
return rethrowUnexpected(err, `readWorkspaceText(${relPath})`);
|
|
50
50
|
}
|
|
@@ -53,7 +53,7 @@ export async function readWorkspaceText(relPath: string): Promise<string | null>
|
|
|
53
53
|
/** Sync variant. Same ENOENT-only swallow contract. */
|
|
54
54
|
export function readWorkspaceTextSync(relPath: string): string | null {
|
|
55
55
|
try {
|
|
56
|
-
return
|
|
56
|
+
return readFileSync(resolveWorkspacePath(relPath), "utf-8");
|
|
57
57
|
} catch (err) {
|
|
58
58
|
return rethrowUnexpected(err, `readWorkspaceTextSync(${relPath})`);
|
|
59
59
|
}
|
|
@@ -133,7 +133,7 @@ export function resolvePath(root: string, relPath: string): string {
|
|
|
133
133
|
* unexpected errors. */
|
|
134
134
|
export async function readTextUnder(root: string, relPath: string): Promise<string | null> {
|
|
135
135
|
try {
|
|
136
|
-
return await
|
|
136
|
+
return await promises.readFile(path.join(root, relPath), "utf-8");
|
|
137
137
|
} catch (err) {
|
|
138
138
|
return rethrowUnexpected(err, `readTextUnder(${relPath})`);
|
|
139
139
|
}
|
|
@@ -147,7 +147,7 @@ export async function writeTextUnder(root: string, relPath: string, content: str
|
|
|
147
147
|
/** Sync read text under a root. Null on ENOENT. */
|
|
148
148
|
export function readTextUnderSync(root: string, relPath: string): string | null {
|
|
149
149
|
try {
|
|
150
|
-
return
|
|
150
|
+
return readFileSync(path.join(root, relPath), "utf-8");
|
|
151
151
|
} catch (err) {
|
|
152
152
|
return rethrowUnexpected(err, `readTextUnderSync(${relPath})`);
|
|
153
153
|
}
|
|
@@ -156,7 +156,7 @@ export function readTextUnderSync(root: string, relPath: string): string | null
|
|
|
156
156
|
/** Sync readdir under a root. Empty on ENOENT. */
|
|
157
157
|
export function readdirUnderSync(root: string, relPath: string): string[] {
|
|
158
158
|
try {
|
|
159
|
-
return
|
|
159
|
+
return readdirSync(path.join(root, relPath));
|
|
160
160
|
} catch (err) {
|
|
161
161
|
if (isEnoent(err)) return [];
|
|
162
162
|
log.error("workspace-io", `readdirUnderSync(${relPath})`, {
|
|
@@ -169,7 +169,7 @@ export function readdirUnderSync(root: string, relPath: string): string[] {
|
|
|
169
169
|
/** Readdir under a root. Empty on ENOENT; rethrows unexpected. */
|
|
170
170
|
export async function readdirUnder(root: string, relPath: string): Promise<string[]> {
|
|
171
171
|
try {
|
|
172
|
-
return await
|
|
172
|
+
return await promises.readdir(path.join(root, relPath));
|
|
173
173
|
} catch (err) {
|
|
174
174
|
if (isEnoent(err)) return [];
|
|
175
175
|
log.error("workspace-io", `readdirUnder(${relPath})`, {
|
|
@@ -180,9 +180,9 @@ export async function readdirUnder(root: string, relPath: string): Promise<strin
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/** Stat under a root. Null on ENOENT; rethrows unexpected. */
|
|
183
|
-
export async function statUnder(root: string, relPath: string): Promise<
|
|
183
|
+
export async function statUnder(root: string, relPath: string): Promise<Stats | null> {
|
|
184
184
|
try {
|
|
185
|
-
return await
|
|
185
|
+
return await promises.stat(path.join(root, relPath));
|
|
186
186
|
} catch (err) {
|
|
187
187
|
return rethrowUnexpected(err, `statUnder(${relPath})`);
|
|
188
188
|
}
|
|
@@ -190,7 +190,7 @@ export async function statUnder(root: string, relPath: string): Promise<fs.Stats
|
|
|
190
190
|
|
|
191
191
|
/** Ensure a directory exists under a root. */
|
|
192
192
|
export async function ensureDirUnder(root: string, relPath: string): Promise<void> {
|
|
193
|
-
await
|
|
193
|
+
await promises.mkdir(path.join(root, relPath), { recursive: true });
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
// ── Existence ───────────────────────────────────────────────────
|
|
@@ -201,7 +201,7 @@ export async function ensureDirUnder(root: string, relPath: string): Promise<voi
|
|
|
201
201
|
*/
|
|
202
202
|
export function existsInWorkspace(relPath: string): boolean {
|
|
203
203
|
try {
|
|
204
|
-
|
|
204
|
+
statSync(resolveWorkspacePath(relPath));
|
|
205
205
|
return true;
|
|
206
206
|
} catch (err) {
|
|
207
207
|
if (isEnoent(err)) return false;
|
|
@@ -217,5 +217,5 @@ export function existsInWorkspace(relPath: string): boolean {
|
|
|
217
217
|
* (including parents) if missing. Idempotent.
|
|
218
218
|
*/
|
|
219
219
|
export function ensureWorkspaceDir(relPath: string): void {
|
|
220
|
-
|
|
220
|
+
mkdirSync(resolveWorkspacePath(relPath), { recursive: true });
|
|
221
221
|
}
|
package/server/utils/gemini.ts
CHANGED
|
@@ -37,8 +37,8 @@ export async function generateGeminiImageContent(
|
|
|
37
37
|
config?: GenerateContentParameters["config"],
|
|
38
38
|
model: string = DEFAULT_IMAGE_MODEL,
|
|
39
39
|
): Promise<GeminiImageResult> {
|
|
40
|
-
const
|
|
41
|
-
const response = await
|
|
40
|
+
const client = getGeminiClient();
|
|
41
|
+
const response = await client.models.generateContent({
|
|
42
42
|
model,
|
|
43
43
|
contents,
|
|
44
44
|
...(config && { config }),
|
|
@@ -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/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
|
}
|
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
|
+
}
|
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/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). */
|