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
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
//
|
|
7
7
|
// NOTE: packages/relay/src/client.ts is a parallel implementation
|
|
8
8
|
// for browser/edge environments using the global WebSocket API.
|
|
9
|
-
// This module uses the `
|
|
9
|
+
// This module uses the `socket` npm package for Node.js. If you change
|
|
10
10
|
// reconnection logic or URL handling here, check the other file too.
|
|
11
11
|
|
|
12
12
|
import WebSocket from "ws";
|
|
13
13
|
import type { ChatService } from "@mulmobridge/chat-service";
|
|
14
14
|
import { ONE_SECOND_MS } from "../utils/time.js";
|
|
15
|
+
import { errorMessage } from "../utils/errors.js";
|
|
15
16
|
|
|
16
17
|
type RelayFn = ChatService["relay"];
|
|
17
18
|
|
|
@@ -87,7 +88,7 @@ function isRelayMessage(value: unknown): value is RelayMessage {
|
|
|
87
88
|
export function connectRelay(deps: RelayClientDeps): RelayClientHandle {
|
|
88
89
|
const { relayUrl, relayToken, relay, logger } = deps;
|
|
89
90
|
|
|
90
|
-
let
|
|
91
|
+
let socket: WebSocket | null = null;
|
|
91
92
|
let reconnectMs = MIN_RECONNECT_MS;
|
|
92
93
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
93
94
|
let stopped = false;
|
|
@@ -103,27 +104,27 @@ export function connectRelay(deps: RelayClientDeps): RelayClientHandle {
|
|
|
103
104
|
if (stopped) return;
|
|
104
105
|
|
|
105
106
|
try {
|
|
106
|
-
|
|
107
|
+
socket = new WebSocket(buildUrl());
|
|
107
108
|
} catch (err) {
|
|
108
109
|
logger.error(LOG_PREFIX, "failed to create WebSocket", {
|
|
109
|
-
error:
|
|
110
|
+
error: errorMessage(err),
|
|
110
111
|
});
|
|
111
112
|
scheduleReconnect();
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
socket.on("open", () => {
|
|
116
117
|
logger.info(LOG_PREFIX, "connected", { url: relayUrl });
|
|
117
118
|
reconnectMs = MIN_RECONNECT_MS;
|
|
118
119
|
flushResponseQueue();
|
|
119
120
|
});
|
|
120
121
|
|
|
121
|
-
|
|
122
|
+
socket.on("message", (data) => {
|
|
122
123
|
handleMessage(String(data));
|
|
123
124
|
});
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
socket.on("close", (code, reason) => {
|
|
127
|
+
socket = null;
|
|
127
128
|
if (TERMINAL_CLOSE_CODES.has(code)) {
|
|
128
129
|
logger.error(LOG_PREFIX, "terminal close, not reconnecting", {
|
|
129
130
|
code,
|
|
@@ -138,7 +139,7 @@ export function connectRelay(deps: RelayClientDeps): RelayClientHandle {
|
|
|
138
139
|
scheduleReconnect();
|
|
139
140
|
});
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
socket.on("error", (err) => {
|
|
142
143
|
logger.warn(LOG_PREFIX, "connection error", {
|
|
143
144
|
error: err.message,
|
|
144
145
|
});
|
|
@@ -204,7 +205,7 @@ export function connectRelay(deps: RelayClientDeps): RelayClientHandle {
|
|
|
204
205
|
} catch (err) {
|
|
205
206
|
logger.error(LOG_PREFIX, "relay processing failed", {
|
|
206
207
|
id: msg.id,
|
|
207
|
-
error:
|
|
208
|
+
error: errorMessage(err),
|
|
208
209
|
});
|
|
209
210
|
sendResponse({
|
|
210
211
|
platform: msg.platform,
|
|
@@ -227,9 +228,9 @@ export function connectRelay(deps: RelayClientDeps): RelayClientHandle {
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
function trySend(response: RelayResponse): boolean {
|
|
230
|
-
if (!
|
|
231
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) return false;
|
|
231
232
|
try {
|
|
232
|
-
|
|
233
|
+
socket.send(JSON.stringify(response), (err) => {
|
|
233
234
|
if (err && !stopped) {
|
|
234
235
|
logger.warn(LOG_PREFIX, "send failed, requeueing", {
|
|
235
236
|
platform: response.platform,
|
|
@@ -261,7 +262,7 @@ export function connectRelay(deps: RelayClientDeps): RelayClientHandle {
|
|
|
261
262
|
count: responseQueue.length,
|
|
262
263
|
});
|
|
263
264
|
while (responseQueue.length > 0) {
|
|
264
|
-
if (!
|
|
265
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) break;
|
|
265
266
|
const response = responseQueue[0]!;
|
|
266
267
|
if (!trySend(response)) break;
|
|
267
268
|
responseQueue.shift();
|
|
@@ -274,9 +275,9 @@ export function connectRelay(deps: RelayClientDeps): RelayClientHandle {
|
|
|
274
275
|
clearTimeout(reconnectTimer);
|
|
275
276
|
reconnectTimer = null;
|
|
276
277
|
}
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
if (socket) {
|
|
279
|
+
socket.close(1000, "shutdown");
|
|
280
|
+
socket = null;
|
|
280
281
|
}
|
|
281
282
|
logger.info(LOG_PREFIX, "stopped");
|
|
282
283
|
}
|
|
@@ -52,16 +52,16 @@ function logsDir(root = workspacePath): string {
|
|
|
52
52
|
// ── I/O deps (real filesystem) ───────────────────────────────────
|
|
53
53
|
|
|
54
54
|
const stateDeps: StateDeps = {
|
|
55
|
-
readFile: (
|
|
56
|
-
writeFileAtomic: (
|
|
55
|
+
readFile: (filePath: string) => readFile(filePath, "utf-8"),
|
|
56
|
+
writeFileAtomic: (filePath: string, content: string) => writeFileAtomic(filePath, content),
|
|
57
57
|
exists: existsSync,
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
const logDeps: LogDeps = {
|
|
61
|
-
appendFile: (
|
|
62
|
-
readFile: (
|
|
61
|
+
appendFile: (filePath: string, content: string) => appendFile(filePath, content),
|
|
62
|
+
readFile: (filePath: string) => readFile(filePath, "utf-8"),
|
|
63
63
|
exists: existsSync,
|
|
64
|
-
ensureDir: (
|
|
64
|
+
ensureDir: (directoryPath: string) => mkdir(directoryPath, { recursive: true }).then(() => {}),
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
// ── System task registry ─────────────────────────────────────────
|
|
@@ -95,11 +95,11 @@ export async function initScheduler(taskManager: ITaskManager, tasks: SystemTask
|
|
|
95
95
|
taskManagerRef = taskManager;
|
|
96
96
|
|
|
97
97
|
// Run catch-up
|
|
98
|
-
const catchUpTasks: CatchUpTask[] = tasks.map((
|
|
99
|
-
id:
|
|
100
|
-
name:
|
|
101
|
-
schedule: toCoreSchedule(
|
|
102
|
-
missedRunPolicy:
|
|
98
|
+
const catchUpTasks: CatchUpTask[] = tasks.map((taskDef) => ({
|
|
99
|
+
id: taskDef.id,
|
|
100
|
+
name: taskDef.name,
|
|
101
|
+
schedule: toCoreSchedule(taskDef.schedule),
|
|
102
|
+
missedRunPolicy: taskDef.missedRunPolicy,
|
|
103
103
|
enabled: true,
|
|
104
104
|
}));
|
|
105
105
|
const plan = computeCatchUpPlan(catchUpTasks, stateMap, Date.now());
|
|
@@ -117,7 +117,7 @@ export async function initScheduler(taskManager: ITaskManager, tasks: SystemTask
|
|
|
117
117
|
runs: plan.runs.length,
|
|
118
118
|
});
|
|
119
119
|
for (const run of plan.runs) {
|
|
120
|
-
const task = tasks.find((
|
|
120
|
+
const task = tasks.find((taskDef) => taskDef.id === run.taskId);
|
|
121
121
|
if (!task) continue;
|
|
122
122
|
await executeAndLog(task, run.context.scheduledFor, TASK_TRIGGERS.catchUp);
|
|
123
123
|
}
|
|
@@ -137,7 +137,7 @@ export async function initScheduler(taskManager: ITaskManager, tasks: SystemTask
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
log.info("scheduler", "initialized", {
|
|
140
|
-
tasks: tasks.map((
|
|
140
|
+
tasks: tasks.map((taskDef) => taskDef.id),
|
|
141
141
|
stateEntries: stateMap.size,
|
|
142
142
|
});
|
|
143
143
|
}
|
|
@@ -146,7 +146,7 @@ export async function initScheduler(taskManager: ITaskManager, tasks: SystemTask
|
|
|
146
146
|
* Updates the in-memory task definition, the task-manager, and
|
|
147
147
|
* recalculates nextScheduledAt in persisted state. */
|
|
148
148
|
export async function applyScheduleOverride(taskId: string, schedule: SystemTaskDef["schedule"]): Promise<boolean> {
|
|
149
|
-
const task = systemTasks.find((
|
|
149
|
+
const task = systemTasks.find((taskDef) => taskDef.id === taskId);
|
|
150
150
|
if (!task || !taskManagerRef) return false;
|
|
151
151
|
if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;
|
|
152
152
|
task.schedule = schedule;
|
|
@@ -172,13 +172,13 @@ export function getSchedulerTasks(): Array<{
|
|
|
172
172
|
missedRunPolicy: string;
|
|
173
173
|
state: TaskExecutionState;
|
|
174
174
|
}> {
|
|
175
|
-
return systemTasks.map((
|
|
176
|
-
id:
|
|
177
|
-
name:
|
|
178
|
-
description:
|
|
179
|
-
schedule:
|
|
180
|
-
missedRunPolicy:
|
|
181
|
-
state: stateMap.get(
|
|
175
|
+
return systemTasks.map((taskDef) => ({
|
|
176
|
+
id: taskDef.id,
|
|
177
|
+
name: taskDef.name,
|
|
178
|
+
description: taskDef.description,
|
|
179
|
+
schedule: taskDef.schedule,
|
|
180
|
+
missedRunPolicy: taskDef.missedRunPolicy,
|
|
181
|
+
state: stateMap.get(taskDef.id) ?? emptyState(taskDef.id),
|
|
182
182
|
}));
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -65,8 +65,8 @@ const storelessPending = new Map<string, Set<string>>();
|
|
|
65
65
|
let pubsub: IPubSub | null = null;
|
|
66
66
|
let evictionTimer: ReturnType<typeof setInterval> | null = null;
|
|
67
67
|
|
|
68
|
-
export function initSessionStore(
|
|
69
|
-
pubsub =
|
|
68
|
+
export function initSessionStore(pubSubInstance: IPubSub): void {
|
|
69
|
+
pubsub = pubSubInstance;
|
|
70
70
|
if (evictionTimer) clearInterval(evictionTimer);
|
|
71
71
|
evictionTimer = setInterval(evictIdleSessions, EVICTION_CHECK_INTERVAL_MS);
|
|
72
72
|
}
|
|
@@ -288,8 +288,8 @@ interface GenerationPayload {
|
|
|
288
288
|
|
|
289
289
|
const GENERATION_KIND_VALUES: ReadonlySet<string> = new Set(Object.values(GENERATION_KINDS));
|
|
290
290
|
|
|
291
|
-
function isGenerationKind(
|
|
292
|
-
return typeof
|
|
291
|
+
function isGenerationKind(value: unknown): value is GenerationKind {
|
|
292
|
+
return typeof value === "string" && GENERATION_KIND_VALUES.has(value);
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/**
|
|
@@ -314,7 +314,7 @@ function applyEventToSession(session: ServerSession, type: string, event: Record
|
|
|
314
314
|
timestamp: Date.now(),
|
|
315
315
|
});
|
|
316
316
|
} else if (type === EVENT_TYPES.toolCallResult) {
|
|
317
|
-
const entry = session.toolCallHistory.find((
|
|
317
|
+
const entry = session.toolCallHistory.find((historyEntry) => historyEntry.toolUseId === event.toolUseId);
|
|
318
318
|
if (entry) entry.result = event.content as string;
|
|
319
319
|
} else if (type === EVENT_TYPES.status) {
|
|
320
320
|
session.statusMessage = event.message as string;
|
|
@@ -404,8 +404,8 @@ export function getSessionImageData(chatSessionId: string): string | undefined {
|
|
|
404
404
|
|
|
405
405
|
export function getActiveSessionIds(): Set<string> {
|
|
406
406
|
const ids = new Set<string>();
|
|
407
|
-
for (const [
|
|
408
|
-
if (session.isRunning) ids.add(
|
|
407
|
+
for (const [chatSessionId, session] of store) {
|
|
408
|
+
if (session.isRunning) ids.add(chatSessionId);
|
|
409
409
|
}
|
|
410
410
|
return ids;
|
|
411
411
|
}
|
|
@@ -465,14 +465,14 @@ function notifySessionsChanged(): void {
|
|
|
465
465
|
|
|
466
466
|
function evictIdleSessions(): void {
|
|
467
467
|
const now = Date.now();
|
|
468
|
-
for (const [
|
|
468
|
+
for (const [chatSessionId, session] of store) {
|
|
469
469
|
if (session.isRunning) continue;
|
|
470
470
|
const age = now - new Date(session.updatedAt).getTime();
|
|
471
471
|
if (age > IDLE_EVICTION_MS) {
|
|
472
472
|
log.info("session-store", "evicting idle session", {
|
|
473
|
-
chatSessionId
|
|
473
|
+
chatSessionId,
|
|
474
474
|
});
|
|
475
|
-
removeSession(
|
|
475
|
+
removeSession(chatSessionId);
|
|
476
476
|
}
|
|
477
477
|
}
|
|
478
478
|
}
|
|
@@ -52,8 +52,8 @@ function isDue(now: Date, schedule: TaskSchedule, tickMs: number): boolean {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (schedule.type === SCHEDULE_TYPES.daily) {
|
|
55
|
-
const [
|
|
56
|
-
const targetMs =
|
|
55
|
+
const [hours, minutes] = schedule.time.split(":").map(Number);
|
|
56
|
+
const targetMs = hours * ONE_HOUR_MS + minutes * ONE_MINUTE_MS;
|
|
57
57
|
const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS;
|
|
58
58
|
const rounded = Math.floor(msSinceMidnight / tickMs) * tickMs;
|
|
59
59
|
return rounded === targetMs;
|
|
@@ -170,11 +170,11 @@ export function createTaskManager(options?: TaskManagerOptions): ITaskManager {
|
|
|
170
170
|
},
|
|
171
171
|
|
|
172
172
|
listTasks() {
|
|
173
|
-
return [...registry.values()].map((
|
|
174
|
-
id:
|
|
175
|
-
description:
|
|
176
|
-
schedule:
|
|
177
|
-
dependsOn:
|
|
173
|
+
return [...registry.values()].map((taskDef) => ({
|
|
174
|
+
id: taskDef.id,
|
|
175
|
+
description: taskDef.description,
|
|
176
|
+
schedule: taskDef.schedule,
|
|
177
|
+
dependsOn: taskDef.dependsOn,
|
|
178
178
|
}));
|
|
179
179
|
},
|
|
180
180
|
};
|
package/server/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
2
|
import express, { Request, Response, NextFunction } from "express";
|
|
3
|
-
import net from "net";
|
|
4
3
|
import path from "path";
|
|
5
4
|
import { fileURLToPath } from "url";
|
|
6
5
|
import agentRoutes from "./api/routes/agent.js";
|
|
@@ -34,8 +33,8 @@ import { mcpToolsRouter, mcpTools, isMcpToolEnabled } from "./agent/mcp-tools/in
|
|
|
34
33
|
import { initWorkspace, workspacePath } from "./workspace/workspace.js";
|
|
35
34
|
import { env, isGeminiAvailable } from "./system/env.js";
|
|
36
35
|
import { buildSandboxStatus } from "./api/sandboxStatus.js";
|
|
37
|
-
import
|
|
38
|
-
import
|
|
36
|
+
import { existsSync, readFileSync } from "fs";
|
|
37
|
+
import { homedir } from "os";
|
|
39
38
|
import { isDockerAvailable, ensureSandboxImage } from "./system/docker.js";
|
|
40
39
|
import { maybeRunJournal } from "./workspace/journal/index.js";
|
|
41
40
|
import { backfillAllSessions } from "./workspace/chat-index/index.js";
|
|
@@ -53,6 +52,7 @@ import { requireSameOrigin } from "./api/csrfGuard.js";
|
|
|
53
52
|
import { bearerAuth } from "./api/auth/bearerAuth.js";
|
|
54
53
|
import { deleteTokenFile, generateAndWriteToken, getCurrentToken } from "./api/auth/token.js";
|
|
55
54
|
import { log } from "./system/logger/index.js";
|
|
55
|
+
import { logBackgroundError } from "./utils/logBackgroundError.js";
|
|
56
56
|
import { startChat } from "./api/routes/agent.js";
|
|
57
57
|
import { registerScheduledSkills } from "./workspace/skills/scheduler.js";
|
|
58
58
|
import { registerUserTasks } from "./workspace/skills/user-tasks.js";
|
|
@@ -60,6 +60,7 @@ import { API_ROUTES } from "../src/config/apiRoutes.js";
|
|
|
60
60
|
import { EVENT_TYPES } from "../src/types/events.js";
|
|
61
61
|
import { SESSION_ORIGINS } from "../src/types/session.js";
|
|
62
62
|
import { ONE_SECOND_MS, ONE_MINUTE_MS, ONE_HOUR_MS } from "./utils/time.js";
|
|
63
|
+
import { isPortFree, findAvailablePort, MAX_PORT_PROBES } from "./utils/port.mjs";
|
|
63
64
|
import { SCHEDULE_TYPES, MISSED_RUN_POLICIES } from "@receptron/task-scheduler";
|
|
64
65
|
|
|
65
66
|
const HTML_TOKEN_PLACEHOLDER = "__MULMOCLAUDE_AUTH_TOKEN__";
|
|
@@ -74,7 +75,6 @@ initWorkspace();
|
|
|
74
75
|
let sandboxEnabled = false;
|
|
75
76
|
|
|
76
77
|
const app = express();
|
|
77
|
-
const PORT = env.port;
|
|
78
78
|
|
|
79
79
|
app.disable("x-powered-by");
|
|
80
80
|
// No `cors()` middleware. The Vite dev proxy forwards `/api/*`
|
|
@@ -157,13 +157,13 @@ app.use(configRoutes);
|
|
|
157
157
|
app.use(skillsRoutes);
|
|
158
158
|
async function listSessionsForBridge(opts: { limit: number; offset: number }) {
|
|
159
159
|
const rows = await loadAllSessions();
|
|
160
|
-
const sorted = rows.sort((
|
|
160
|
+
const sorted = rows.sort((leftSession, rightSession) => rightSession.changeMs - leftSession.changeMs);
|
|
161
161
|
const total = sorted.length;
|
|
162
|
-
const sessions = sorted.slice(opts.offset, opts.offset + opts.limit).map((
|
|
163
|
-
id:
|
|
164
|
-
roleId:
|
|
165
|
-
preview:
|
|
166
|
-
updatedAt:
|
|
162
|
+
const sessions = sorted.slice(opts.offset, opts.offset + opts.limit).map((row) => ({
|
|
163
|
+
id: row.summary.id,
|
|
164
|
+
roleId: row.summary.roleId,
|
|
165
|
+
preview: row.summary.preview,
|
|
166
|
+
updatedAt: row.summary.updatedAt,
|
|
167
167
|
}));
|
|
168
168
|
return { sessions, total };
|
|
169
169
|
}
|
|
@@ -234,7 +234,7 @@ if (env.isProduction) {
|
|
|
234
234
|
app.get("/{*splat}", (_req: Request, res: Response) => {
|
|
235
235
|
let html: string;
|
|
236
236
|
try {
|
|
237
|
-
html =
|
|
237
|
+
html = readFileSync(indexHtmlPath, "utf-8");
|
|
238
238
|
} catch (err) {
|
|
239
239
|
log.error("server", "failed to read index.html", { error: String(err) });
|
|
240
240
|
serverError(res, "Internal Server Error");
|
|
@@ -255,28 +255,40 @@ app.use((err: Error, _req: Request, res: Response, __next: NextFunction) => {
|
|
|
255
255
|
serverError(res, "Internal Server Error");
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
258
|
+
// True iff the user set `PORT` explicitly; empty string counts as "not
|
|
259
|
+
// set". We use this to decide between "walk forward when busy" (friendly
|
|
260
|
+
// dev behaviour) and "fail loudly" (respect the user's choice).
|
|
261
|
+
const portExplicit = typeof process.env.PORT === "string" && process.env.PORT.trim() !== "";
|
|
262
|
+
|
|
263
|
+
// Resolve the port we'll actually bind to. Default PORT (3001) + busy
|
|
264
|
+
// walks forward so a stale `yarn dev` or a parallel test run doesn't
|
|
265
|
+
// crash the launch. Explicit PORT + busy exits — matches the launcher's
|
|
266
|
+
// `--port` semantics so `PORT=3099 yarn dev` behaves the same as
|
|
267
|
+
// `npx mulmoclaude --port 3099`.
|
|
268
|
+
async function resolvePort(): Promise<number> {
|
|
269
|
+
const requested = env.port;
|
|
270
|
+
if (await isPortFree(requested)) return requested;
|
|
271
|
+
if (portExplicit) {
|
|
272
|
+
log.error("server", `Port ${requested} is already in use. Stop the other process or pick a different PORT.`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
const fallback = await findAvailablePort(requested + 1);
|
|
276
|
+
if (fallback === null) {
|
|
277
|
+
log.error("server", `Port ${requested} is in use and no free port found in ${requested}..${requested + MAX_PORT_PROBES - 1}.`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
log.info("server", `Port ${requested} busy → using ${fallback} instead`);
|
|
281
|
+
return fallback;
|
|
270
282
|
}
|
|
271
283
|
|
|
272
284
|
async function ensureCredentialsAvailable(): Promise<void> {
|
|
273
|
-
const credentialsPath = path.join(
|
|
274
|
-
if (
|
|
285
|
+
const credentialsPath = path.join(homedir(), ".claude", ".credentials.json");
|
|
286
|
+
if (existsSync(credentialsPath)) return;
|
|
275
287
|
|
|
276
288
|
if (process.platform === "darwin") {
|
|
277
289
|
const { refreshCredentials } = await import("./system/credentials.js");
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
290
|
+
const refreshSucceeded = await refreshCredentials();
|
|
291
|
+
if (refreshSucceeded) return;
|
|
280
292
|
log.error("sandbox", "Failed to export credentials from macOS Keychain. Run `npm run sandbox:login` manually.");
|
|
281
293
|
process.exit(1);
|
|
282
294
|
}
|
|
@@ -310,14 +322,14 @@ async function setupSandbox(): Promise<boolean> {
|
|
|
310
322
|
|
|
311
323
|
function logMcpStatus(): void {
|
|
312
324
|
const enabledMcpTools = mcpTools.filter(isMcpToolEnabled);
|
|
313
|
-
const disabledMcpTools = mcpTools.filter((
|
|
325
|
+
const disabledMcpTools = mcpTools.filter((toolDef) => !isMcpToolEnabled(toolDef));
|
|
314
326
|
if (enabledMcpTools.length > 0) {
|
|
315
327
|
log.info("mcp", "Available", {
|
|
316
|
-
tools: enabledMcpTools.map((
|
|
328
|
+
tools: enabledMcpTools.map((toolDef) => toolDef.definition.name).join(", "),
|
|
317
329
|
});
|
|
318
330
|
}
|
|
319
331
|
if (disabledMcpTools.length > 0) {
|
|
320
|
-
const names = disabledMcpTools.map((
|
|
332
|
+
const names = disabledMcpTools.map((toolDef) => toolDef.definition.name + " (" + (toolDef.requiredEnv ?? []).join(", ") + ")").join(", ");
|
|
321
333
|
log.info("mcp", "Unavailable (missing env)", { tools: names });
|
|
322
334
|
}
|
|
323
335
|
}
|
|
@@ -329,9 +341,7 @@ function maybeForceJournalRun(): void {
|
|
|
329
341
|
// propagate out of maybeRunJournal.
|
|
330
342
|
if (!env.journalForceRunOnStartup) return;
|
|
331
343
|
log.info("journal", "JOURNAL_FORCE_RUN_ON_STARTUP=1 — running now");
|
|
332
|
-
maybeRunJournal({ force: true }).catch((
|
|
333
|
-
log.warn("journal", "forced startup run failed", { error: String(err) });
|
|
334
|
-
});
|
|
344
|
+
maybeRunJournal({ force: true }).catch(logBackgroundError("journal", "forced startup run failed"));
|
|
335
345
|
}
|
|
336
346
|
|
|
337
347
|
function maybeForceChatIndexBackfill(): void {
|
|
@@ -349,15 +359,11 @@ function maybeForceChatIndexBackfill(): void {
|
|
|
349
359
|
skipped: result.skipped,
|
|
350
360
|
});
|
|
351
361
|
})
|
|
352
|
-
.catch((
|
|
353
|
-
log.warn("chat-index", "forced startup backfill failed", {
|
|
354
|
-
error: String(err),
|
|
355
|
-
});
|
|
356
|
-
});
|
|
362
|
+
.catch(logBackgroundError("chat-index", "forced startup backfill failed"));
|
|
357
363
|
}
|
|
358
364
|
|
|
359
|
-
function startRuntimeServices(httpServer: ReturnType<typeof app.listen
|
|
360
|
-
log.info("server", "listening", { port
|
|
365
|
+
function startRuntimeServices(httpServer: ReturnType<typeof app.listen>, port: number): void {
|
|
366
|
+
log.info("server", "listening", { port });
|
|
361
367
|
|
|
362
368
|
// --- Pub/Sub ---
|
|
363
369
|
const pubsub = createPubSub(httpServer);
|
|
@@ -424,24 +430,24 @@ function startRuntimeServices(httpServer: ReturnType<typeof app.listen>): void {
|
|
|
424
430
|
// are silently ignored — the hardcoded defaults above remain.
|
|
425
431
|
const overrides = loadSchedulerOverrides();
|
|
426
432
|
for (const task of systemTasks) {
|
|
427
|
-
const
|
|
428
|
-
if (!
|
|
429
|
-
if (task.schedule.type === SCHEDULE_TYPES.interval && typeof
|
|
433
|
+
const override = overrides[task.id];
|
|
434
|
+
if (!override) continue;
|
|
435
|
+
if (task.schedule.type === SCHEDULE_TYPES.interval && typeof override.intervalMs === "number" && override.intervalMs > 0) {
|
|
430
436
|
log.info("scheduler", "applying override", {
|
|
431
437
|
id: task.id,
|
|
432
|
-
intervalMs:
|
|
438
|
+
intervalMs: override.intervalMs,
|
|
433
439
|
});
|
|
434
440
|
task.schedule = {
|
|
435
441
|
type: SCHEDULE_TYPES.interval,
|
|
436
|
-
intervalMs:
|
|
442
|
+
intervalMs: override.intervalMs,
|
|
437
443
|
};
|
|
438
444
|
}
|
|
439
|
-
if (task.schedule.type === SCHEDULE_TYPES.daily && typeof
|
|
445
|
+
if (task.schedule.type === SCHEDULE_TYPES.daily && typeof override.time === "string" && UTC_HH_MM_RE.test(override.time)) {
|
|
440
446
|
log.info("scheduler", "applying override", {
|
|
441
447
|
id: task.id,
|
|
442
|
-
time:
|
|
448
|
+
time: override.time,
|
|
443
449
|
});
|
|
444
|
-
task.schedule = { type: SCHEDULE_TYPES.daily, time:
|
|
450
|
+
task.schedule = { type: SCHEDULE_TYPES.daily, time: override.time };
|
|
445
451
|
}
|
|
446
452
|
}
|
|
447
453
|
|
|
@@ -464,11 +470,7 @@ function startRuntimeServices(httpServer: ReturnType<typeof app.listen>): void {
|
|
|
464
470
|
log.info("skills", "scheduled skills registered", { count });
|
|
465
471
|
}
|
|
466
472
|
})
|
|
467
|
-
.catch((
|
|
468
|
-
log.warn("skills", "failed to register scheduled skills", {
|
|
469
|
-
error: String(err),
|
|
470
|
-
});
|
|
471
|
-
});
|
|
473
|
+
.catch(logBackgroundError("skills", "failed to register scheduled skills"));
|
|
472
474
|
|
|
473
475
|
// Register user-created scheduled tasks from tasks.json.
|
|
474
476
|
registerUserTasks({ taskManager, startChat })
|
|
@@ -477,11 +479,7 @@ function startRuntimeServices(httpServer: ReturnType<typeof app.listen>): void {
|
|
|
477
479
|
log.info("user-tasks", "user tasks registered", { count });
|
|
478
480
|
}
|
|
479
481
|
})
|
|
480
|
-
.catch((
|
|
481
|
-
log.warn("user-tasks", "failed to register user tasks", {
|
|
482
|
-
error: String(err),
|
|
483
|
-
});
|
|
484
|
-
});
|
|
482
|
+
.catch(logBackgroundError("user-tasks", "failed to register user tasks"));
|
|
485
483
|
|
|
486
484
|
taskManager.start();
|
|
487
485
|
|
|
@@ -510,11 +508,7 @@ process.on("SIGTERM", () => {
|
|
|
510
508
|
});
|
|
511
509
|
|
|
512
510
|
(async () => {
|
|
513
|
-
const
|
|
514
|
-
if (!portFree) {
|
|
515
|
-
log.error("server", `Port ${PORT} is already in use. Stop the other process and try again.`);
|
|
516
|
-
process.exit(1);
|
|
517
|
-
}
|
|
511
|
+
const port = await resolvePort();
|
|
518
512
|
|
|
519
513
|
// Generate the bearer token before `app.listen` so the first
|
|
520
514
|
// request cannot race an uninitialised `getCurrentToken()`. The
|
|
@@ -535,8 +529,8 @@ process.on("SIGTERM", () => {
|
|
|
535
529
|
// `http://<laptop-ip>:3001/api/*`), which combined with the
|
|
536
530
|
// workspace file API is a credential-theft risk. Personal dev
|
|
537
531
|
// tool — localhost is the right default.
|
|
538
|
-
const httpServer = app.listen(
|
|
539
|
-
startRuntimeServices(httpServer);
|
|
532
|
+
const httpServer = app.listen(port, "127.0.0.1", () => {
|
|
533
|
+
startRuntimeServices(httpServer, port);
|
|
540
534
|
});
|
|
541
535
|
})();
|
|
542
536
|
|
package/server/system/config.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// defaults. Writers perform an atomic replace (tmp + rename) so a
|
|
10
10
|
// reader never observes a half-written file.
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
import { mkdirSync } from "fs";
|
|
13
13
|
import path from "path";
|
|
14
14
|
import { log } from "./logger/index.js";
|
|
15
15
|
import { WORKSPACE_PATHS } from "../workspace/paths.js";
|
|
@@ -44,7 +44,7 @@ export function mcpConfigPath(): string {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function ensureConfigsDir(): void {
|
|
47
|
-
|
|
47
|
+
mkdirSync(configsDir(), { recursive: true });
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export function isAppSettings(value: unknown): value is AppSettings {
|
|
@@ -181,8 +181,8 @@ export function isMcpConfigFile(value: unknown): value is McpConfigFile {
|
|
|
181
181
|
|
|
182
182
|
const servers = value.mcpServers;
|
|
183
183
|
if (!isRecord(servers)) return false;
|
|
184
|
-
for (const [
|
|
185
|
-
if (!isMcpServerId(
|
|
184
|
+
for (const [serverId, spec] of Object.entries(servers)) {
|
|
185
|
+
if (!isMcpServerId(serverId)) return false;
|
|
186
186
|
if (!isMcpServerSpec(spec)) return false;
|
|
187
187
|
}
|
|
188
188
|
return true;
|
|
@@ -220,7 +220,7 @@ export function saveMcpConfig(cfg: McpConfigFile): void {
|
|
|
220
220
|
|
|
221
221
|
// Flatten storage form to UI-friendly array.
|
|
222
222
|
export function toMcpEntries(cfg: McpConfigFile): McpServerEntry[] {
|
|
223
|
-
return Object.entries(cfg.mcpServers).map(([
|
|
223
|
+
return Object.entries(cfg.mcpServers).map(([serverId, spec]) => ({ id: serverId, spec }));
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// Re-inflate UI-friendly array to storage form. Duplicate ids are
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execFile } from "child_process";
|
|
2
|
-
import { writeFile } from "fs/promises";
|
|
3
2
|
import { homedir } from "os";
|
|
4
3
|
import { join } from "path";
|
|
5
4
|
import { promisify } from "util";
|
|
6
5
|
import { log } from "./logger/index.js";
|
|
7
6
|
import { ONE_SECOND_MS, ONE_MINUTE_MS } from "../utils/time.js";
|
|
7
|
+
import { writeFileAtomic } from "../utils/files/atomic.js";
|
|
8
8
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
|
|
@@ -121,13 +121,13 @@ async function renewTokenViaPty(): Promise<boolean> {
|
|
|
121
121
|
buffer += data;
|
|
122
122
|
|
|
123
123
|
if (!responded) {
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
124
|
+
const match = ECHO_RE.exec(buffer);
|
|
125
|
+
if (match) {
|
|
126
126
|
// Claude echoed our "hi" — remember where the response
|
|
127
127
|
// window starts so the success check looks only at bytes
|
|
128
128
|
// that arrived AFTER the echo.
|
|
129
129
|
responded = true;
|
|
130
|
-
echoEndIdx =
|
|
130
|
+
echoEndIdx = match.index + match[0].length;
|
|
131
131
|
}
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
@@ -208,7 +208,9 @@ export async function refreshCredentials(): Promise<boolean> {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
// Atomic so a readers mid-refresh can't see a truncated creds
|
|
212
|
+
// file; mode preserves the 0o600 we always set on this file.
|
|
213
|
+
await writeFileAtomic(CREDENTIALS_PATH, credentials + "\n", { mode: 0o600 });
|
|
212
214
|
log.info("credentials", "Fresh credentials written to ~/.claude/.credentials.json");
|
|
213
215
|
return true;
|
|
214
216
|
} catch (err) {
|
package/server/system/env.ts
CHANGED
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
|
|
20
20
|
function asInt(value: string | undefined, fallback: number, opts: { min?: number; max?: number } = {}): number {
|
|
21
21
|
if (value === undefined || value === "") return fallback;
|
|
22
|
-
const
|
|
23
|
-
if (!Number.isInteger(
|
|
24
|
-
if (opts.min !== undefined &&
|
|
25
|
-
if (opts.max !== undefined &&
|
|
26
|
-
return
|
|
22
|
+
const parsed = Number(value);
|
|
23
|
+
if (!Number.isInteger(parsed)) return fallback;
|
|
24
|
+
if (opts.min !== undefined && parsed < opts.min) return fallback;
|
|
25
|
+
if (opts.max !== undefined && parsed > opts.max) return fallback;
|
|
26
|
+
return parsed;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function asFlag(value: string | undefined): boolean {
|