mulmoclaude 0.1.2 → 0.3.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 +1 -1
- package/client/assets/{index-KNLBjwuh.css → index-Bm70FDU2.css} +1 -1
- package/client/assets/{index-D8rhwXLq.js → index-eHWB79u5.js} +3 -3
- package/client/index.html +2 -2
- package/package.json +1 -1
- package/server/agent/config.ts +12 -12
- package/server/agent/mcp-server.ts +19 -19
- package/server/agent/mcp-tools/x.ts +5 -5
- package/server/agent/prompt.ts +9 -4
- package/server/agent/sandboxMounts.ts +7 -7
- package/server/agent/stream.ts +4 -4
- package/server/api/routes/files.ts +9 -9
- package/server/api/routes/scheduler.ts +8 -8
- package/server/api/routes/schedulerHandlers.ts +12 -12
- package/server/api/routes/schedulerTasks.ts +14 -14
- package/server/api/routes/sessions.ts +24 -24
- package/server/api/routes/todosColumnsHandlers.ts +30 -30
- package/server/api/routes/wiki.ts +14 -14
- 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 +19 -19
- package/server/utils/date.ts +18 -18
- package/server/utils/files/atomic.ts +9 -9
- package/server/utils/files/html-io.ts +5 -5
- package/server/utils/files/image-store.ts +2 -2
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/naming.ts +2 -2
- package/server/utils/files/roles-io.ts +10 -10
- package/server/utils/files/scheduler-io.ts +5 -5
- package/server/utils/files/session-io.ts +35 -35
- package/server/utils/files/spreadsheet-store.ts +2 -2
- package/server/utils/files/todos-io.ts +9 -9
- package/server/utils/files/user-tasks-io.ts +5 -5
- package/server/workspace/chat-index/indexer.ts +15 -15
- package/server/workspace/custom-dirs.ts +11 -11
- package/server/workspace/journal/archivist.ts +35 -35
- package/server/workspace/journal/dailyPass.ts +31 -28
- package/server/workspace/journal/indexFile.ts +29 -25
- package/server/workspace/reference-dirs.ts +18 -18
- package/server/workspace/roles.ts +6 -6
- package/server/workspace/skills/discovery.ts +4 -4
- package/server/workspace/skills/user-tasks.ts +34 -34
- package/server/workspace/sources/arxivDiscovery.ts +8 -8
- 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/interests.ts +9 -9
- package/server/workspace/sources/pipeline/index.ts +6 -6
- package/server/workspace/sources/pipeline/plan.ts +5 -5
- package/server/workspace/sources/registry.ts +16 -16
- package/server/workspace/sources/robots.ts +14 -14
- package/server/workspace/sources/sourceState.ts +11 -9
- 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/src/App.vue +30 -30
- package/src/components/ChatInput.vue +7 -7
- package/src/components/LockStatusPopup.vue +2 -2
- package/src/components/NotificationToast.vue +2 -2
- package/src/components/RoleSelector.vue +2 -2
- package/src/components/SessionHistoryPanel.vue +6 -6
- package/src/components/SettingsMcpTab.vue +7 -7
- package/src/components/SettingsModal.vue +3 -3
- package/src/components/SettingsReferenceDirsTab.vue +10 -10
- package/src/components/SettingsWorkspaceDirsTab.vue +5 -5
- package/src/components/SuggestionsPanel.vue +2 -2
- package/src/components/todo/TodoAddDialog.vue +2 -2
- package/src/components/todo/TodoEditPanel.vue +2 -2
- package/src/components/todo/TodoListView.vue +5 -5
- package/src/composables/useCanvasViewMode.ts +5 -5
- package/src/composables/useClickOutside.ts +2 -2
- package/src/composables/useFreshPluginData.ts +3 -3
- package/src/composables/useKeyNavigation.ts +11 -11
- 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/useSessionSync.ts +8 -8
- package/src/composables/useViewLayout.ts +2 -2
- package/src/config/roles.ts +2 -2
- package/src/plugins/chart/Preview.vue +4 -4
- package/src/plugins/manageSkills/View.vue +3 -3
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +2 -2
- package/src/plugins/presentHtml/helpers.ts +8 -8
- package/src/plugins/presentMulmoScript/View.vue +4 -4
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/scheduler/Preview.vue +6 -6
- package/src/plugins/scheduler/TasksTab.vue +4 -4
- package/src/plugins/textResponse/View.vue +2 -2
- package/src/plugins/todo/Preview.vue +2 -2
- package/src/plugins/todo/View.vue +11 -11
- package/src/plugins/todo/composables/useTodos.ts +5 -5
- package/src/plugins/wiki/Preview.vue +5 -5
- package/src/plugins/wiki/helpers.ts +4 -4
- package/src/router/guards.ts +12 -12
- package/src/types/session.ts +4 -3
- package/src/utils/agent/request.ts +3 -3
- 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/rewriteMarkdownImageRefs.ts +5 -5
- package/src/utils/markdown/extractFirstH1.ts +2 -2
- package/src/utils/path/relativeLink.ts +15 -15
- 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/client/index.html
CHANGED
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
<title>MulmoClaude</title>
|
|
18
18
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='30' height='30' x='1' y='1' rx='6' fill='%236B7280'/><text x='16' y='17' text-anchor='middle' dominant-baseline='central' font-family='sans-serif' font-weight='bold' font-size='20' fill='white'>M</text></svg>" />
|
|
19
19
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
20
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
+
<script type="module" crossorigin src="/assets/index-eHWB79u5.js"></script>
|
|
21
21
|
<link rel="modulepreload" crossorigin href="/assets/chunk-vKJrgz-R-C_I3GbVV.js">
|
|
22
22
|
<link rel="modulepreload" crossorigin href="/assets/typeof-DBp4T-Ny-BC0P-2DM.js">
|
|
23
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
23
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bm70FDU2.css">
|
|
24
24
|
</head>
|
|
25
25
|
<body>
|
|
26
26
|
<div id="app"></div>
|
package/package.json
CHANGED
package/server/agent/config.ts
CHANGED
|
@@ -14,10 +14,10 @@ export const CONTAINER_WORKSPACE_PATH = "/home/node/mulmoclaude";
|
|
|
14
14
|
|
|
15
15
|
const BASE_ALLOWED_TOOLS = ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebFetch", "WebSearch"];
|
|
16
16
|
|
|
17
|
-
const MCP_PLUGINS = new Set([...MCP_PLUGIN_NAMES, ...mcpTools.filter(isMcpToolEnabled).map((
|
|
17
|
+
const MCP_PLUGINS = new Set([...MCP_PLUGIN_NAMES, ...mcpTools.filter(isMcpToolEnabled).map((toolDef) => toolDef.definition.name)]);
|
|
18
18
|
|
|
19
19
|
export function getActivePlugins(role: Role): string[] {
|
|
20
|
-
return role.availablePlugins.filter((
|
|
20
|
+
return role.availablePlugins.filter((pluginName) => MCP_PLUGINS.has(pluginName));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface McpConfigParams {
|
|
@@ -71,12 +71,12 @@ function prepareUserStdioServer(spec: Extract<McpServerSpec, { type: "stdio" }>,
|
|
|
71
71
|
|
|
72
72
|
export function prepareUserServers(userServers: Record<string, McpServerSpec>, useDocker: boolean, hostWorkspacePath: string): Record<string, McpServerSpec> {
|
|
73
73
|
const out: Record<string, McpServerSpec> = {};
|
|
74
|
-
for (const [
|
|
74
|
+
for (const [serverId, spec] of Object.entries(userServers)) {
|
|
75
75
|
if (spec.enabled === false) continue;
|
|
76
76
|
if (spec.type === "http") {
|
|
77
|
-
out[
|
|
77
|
+
out[serverId] = prepareUserHttpServer(spec, useDocker);
|
|
78
78
|
} else {
|
|
79
|
-
out[
|
|
79
|
+
out[serverId] = prepareUserStdioServer(spec, useDocker, hostWorkspacePath);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
return out;
|
|
@@ -137,9 +137,9 @@ function buildMulmoclaudeServer(params: { chatSessionId: string; port: number; a
|
|
|
137
137
|
// defence-in-depth.
|
|
138
138
|
function excludeReservedKeys(servers: Record<string, McpServerSpec>): Record<string, McpServerSpec> {
|
|
139
139
|
const out: Record<string, McpServerSpec> = {};
|
|
140
|
-
for (const [
|
|
141
|
-
if (
|
|
142
|
-
out[
|
|
140
|
+
for (const [serverId, spec] of Object.entries(servers)) {
|
|
141
|
+
if (serverId === "mulmoclaude") continue;
|
|
142
|
+
out[serverId] = spec;
|
|
143
143
|
}
|
|
144
144
|
return out;
|
|
145
145
|
}
|
|
@@ -165,12 +165,12 @@ export function buildMcpConfig(params: McpConfigParams): object {
|
|
|
165
165
|
// we're running natively (since the sandbox image is minimal in Docker).
|
|
166
166
|
export function userServerAllowedToolNames(userServers: Record<string, McpServerSpec>, useDocker: boolean): string[] {
|
|
167
167
|
const names: string[] = [];
|
|
168
|
-
for (const [
|
|
168
|
+
for (const [serverId, spec] of Object.entries(userServers)) {
|
|
169
169
|
if (spec.enabled === false) continue;
|
|
170
170
|
// Stdio servers are dropped under Docker because the sandbox
|
|
171
171
|
// image is too minimal to run most of them (see #162).
|
|
172
172
|
if (spec.type === "stdio" && useDocker) continue;
|
|
173
|
-
names.push(`mcp__${
|
|
173
|
+
names.push(`mcp__${serverId}`);
|
|
174
174
|
}
|
|
175
175
|
return names;
|
|
176
176
|
}
|
|
@@ -188,7 +188,7 @@ export interface CliArgsParams {
|
|
|
188
188
|
export function buildCliArgs(params: CliArgsParams): string[] {
|
|
189
189
|
const { systemPrompt, activePlugins, claudeSessionId, mcpConfigPath, extraAllowedTools = [] } = params;
|
|
190
190
|
|
|
191
|
-
const mcpToolNames = activePlugins.map((
|
|
191
|
+
const mcpToolNames = activePlugins.map((pluginName) => `mcp__mulmoclaude__${pluginName}`);
|
|
192
192
|
const allowedTools = [...BASE_ALLOWED_TOOLS, ...extraAllowedTools, ...mcpToolNames];
|
|
193
193
|
|
|
194
194
|
// stream-json input mode: the user message is streamed through
|
|
@@ -351,7 +351,7 @@ export function buildDockerSpawnArgs(params: DockerSpawnArgsParams): string[] {
|
|
|
351
351
|
sandboxAuthArgs = [],
|
|
352
352
|
sshAgentForward = false,
|
|
353
353
|
} = params;
|
|
354
|
-
const toDockerPath = (
|
|
354
|
+
const toDockerPath = (hostPath: string): string => hostPath.replace(/\\/g, "/");
|
|
355
355
|
const extraHosts: string[] = platform === "linux" ? ["--add-host", "host.docker.internal:host-gateway"] : [];
|
|
356
356
|
|
|
357
357
|
return [
|
|
@@ -30,7 +30,7 @@ interface JsonRpcMessage {
|
|
|
30
30
|
params?: ToolCallParams;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const isJsonRpcMessage = (
|
|
33
|
+
const isJsonRpcMessage = (value: unknown): value is JsonRpcMessage => isRecord(value) && "method" in value;
|
|
34
34
|
|
|
35
35
|
const SESSION_ID = env.mcpSessionId;
|
|
36
36
|
const PORT = env.port;
|
|
@@ -80,12 +80,12 @@ function fromPackage(def: ToolDefinition, endpoint: string): ToolDef {
|
|
|
80
80
|
|
|
81
81
|
// Pure MCP tools (no GUI) — auto-registered from server/mcp-tools/
|
|
82
82
|
const mcpToolDefs: Record<string, ToolDef> = Object.fromEntries(
|
|
83
|
-
mcpTools.filter(isMcpToolEnabled).map((
|
|
84
|
-
|
|
83
|
+
mcpTools.filter(isMcpToolEnabled).map((toolDef) => [
|
|
84
|
+
toolDef.definition.name,
|
|
85
85
|
{
|
|
86
|
-
name:
|
|
87
|
-
description:
|
|
88
|
-
inputSchema:
|
|
86
|
+
name: toolDef.definition.name,
|
|
87
|
+
description: toolDef.definition.description,
|
|
88
|
+
inputSchema: toolDef.definition.inputSchema,
|
|
89
89
|
},
|
|
90
90
|
]),
|
|
91
91
|
);
|
|
@@ -303,7 +303,7 @@ async function handleToolCall(name: string, args: Record<string, unknown>): Prom
|
|
|
303
303
|
// Pure MCP tools — call via /api/mcp-tools/:tool, return text directly
|
|
304
304
|
// (no frontend push). Opt out of postJson's HTTP error throw because
|
|
305
305
|
// we want to surface the JSON error body to the caller as a string.
|
|
306
|
-
const mcpTool = mcpTools.find((
|
|
306
|
+
const mcpTool = mcpTools.find((toolDef) => toolDef.definition.name === name);
|
|
307
307
|
if (mcpTool) {
|
|
308
308
|
const res = await postJson(`/api/mcp-tools/${name}`, args, {
|
|
309
309
|
allowHttpError: true,
|
|
@@ -313,7 +313,7 @@ async function handleToolCall(name: string, args: Record<string, unknown>): Prom
|
|
|
313
313
|
return typeof json.result === "string" ? json.result : JSON.stringify(json.result);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
const tool = tools.find((
|
|
316
|
+
const tool = tools.find((toolDef) => toolDef.name === name);
|
|
317
317
|
if (!tool) throw new Error(`Unknown tool: ${name}`);
|
|
318
318
|
|
|
319
319
|
const res = await postJson(tool.endpoint!, args);
|
|
@@ -347,12 +347,12 @@ process.stdin.on("data", (chunk: Buffer) => {
|
|
|
347
347
|
}
|
|
348
348
|
if (!isJsonRpcMessage(msg)) continue;
|
|
349
349
|
|
|
350
|
-
const { id, method, params } = msg;
|
|
350
|
+
const { id: requestId, method, params } = msg;
|
|
351
351
|
|
|
352
352
|
if (method === "initialize") {
|
|
353
353
|
respond({
|
|
354
354
|
jsonrpc: "2.0",
|
|
355
|
-
id,
|
|
355
|
+
id: requestId,
|
|
356
356
|
result: {
|
|
357
357
|
protocolVersion: "2024-11-05",
|
|
358
358
|
capabilities: { tools: {} },
|
|
@@ -362,12 +362,12 @@ process.stdin.on("data", (chunk: Buffer) => {
|
|
|
362
362
|
} else if (method === "tools/list") {
|
|
363
363
|
respond({
|
|
364
364
|
jsonrpc: "2.0",
|
|
365
|
-
id,
|
|
365
|
+
id: requestId,
|
|
366
366
|
result: {
|
|
367
|
-
tools: tools.map((
|
|
368
|
-
name:
|
|
369
|
-
description:
|
|
370
|
-
inputSchema:
|
|
367
|
+
tools: tools.map((toolDef) => ({
|
|
368
|
+
name: toolDef.name,
|
|
369
|
+
description: toolDef.description,
|
|
370
|
+
inputSchema: toolDef.inputSchema,
|
|
371
371
|
})),
|
|
372
372
|
},
|
|
373
373
|
});
|
|
@@ -375,7 +375,7 @@ process.stdin.on("data", (chunk: Buffer) => {
|
|
|
375
375
|
if (!params?.name) {
|
|
376
376
|
respond({
|
|
377
377
|
jsonrpc: "2.0",
|
|
378
|
-
id,
|
|
378
|
+
id: requestId,
|
|
379
379
|
error: {
|
|
380
380
|
code: -32602,
|
|
381
381
|
message: "Invalid params: tools/call requires params.name",
|
|
@@ -388,14 +388,14 @@ process.stdin.on("data", (chunk: Buffer) => {
|
|
|
388
388
|
.then((text) => {
|
|
389
389
|
respond({
|
|
390
390
|
jsonrpc: "2.0",
|
|
391
|
-
id,
|
|
391
|
+
id: requestId,
|
|
392
392
|
result: { content: [{ type: "text", text }] },
|
|
393
393
|
});
|
|
394
394
|
})
|
|
395
395
|
.catch((err: unknown) => {
|
|
396
396
|
respond({
|
|
397
397
|
jsonrpc: "2.0",
|
|
398
|
-
id,
|
|
398
|
+
id: requestId,
|
|
399
399
|
result: {
|
|
400
400
|
content: [{ type: "text", text: String(err) }],
|
|
401
401
|
isError: true,
|
|
@@ -403,7 +403,7 @@ process.stdin.on("data", (chunk: Buffer) => {
|
|
|
403
403
|
});
|
|
404
404
|
});
|
|
405
405
|
} else if (method === "ping") {
|
|
406
|
-
respond({ jsonrpc: "2.0", id, result: {} });
|
|
406
|
+
respond({ jsonrpc: "2.0", id: requestId, result: {} });
|
|
407
407
|
}
|
|
408
408
|
// notifications/initialized and other notifications: no response needed
|
|
409
409
|
}
|
|
@@ -64,7 +64,7 @@ function formatTweet(tweet: XTweet, author?: XUser, url?: string): string {
|
|
|
64
64
|
: "";
|
|
65
65
|
const link = url ?? "";
|
|
66
66
|
return [byline, "", tweet.text, "", metrics, link]
|
|
67
|
-
.filter((
|
|
67
|
+
.filter((line) => line !== undefined)
|
|
68
68
|
.join("\n")
|
|
69
69
|
.trimEnd();
|
|
70
70
|
}
|
|
@@ -104,12 +104,12 @@ export const readXPost = {
|
|
|
104
104
|
return errorMessage(err);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
if (data.errors?.length) return `X API error: ${data.errors.map((
|
|
107
|
+
if (data.errors?.length) return `X API error: ${data.errors.map((err) => err.detail).join("; ")}`;
|
|
108
108
|
|
|
109
109
|
const tweet = data.data as XTweet | undefined;
|
|
110
110
|
if (!tweet) return "Tweet not found.";
|
|
111
111
|
|
|
112
|
-
const author = data.includes?.users?.find((
|
|
112
|
+
const author = data.includes?.users?.find((user) => user.id === tweet.author_id);
|
|
113
113
|
const canonicalUrl = author ? `https://x.com/${author.username}/status/${tweet.id}` : undefined;
|
|
114
114
|
return formatTweet(tweet, author, canonicalUrl);
|
|
115
115
|
},
|
|
@@ -168,13 +168,13 @@ export const searchX = {
|
|
|
168
168
|
return errorMessage(err);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
if (data.errors?.length) return `X API error: ${data.errors.map((
|
|
171
|
+
if (data.errors?.length) return `X API error: ${data.errors.map((err) => err.detail).join("; ")}`;
|
|
172
172
|
|
|
173
173
|
const tweets = Array.isArray(data.data) ? data.data : [];
|
|
174
174
|
if (tweets.length === 0) return `No recent posts found for: "${query}"`;
|
|
175
175
|
|
|
176
176
|
const users = data.includes?.users ?? [];
|
|
177
|
-
const userMap = new Map(users.map((
|
|
177
|
+
const userMap = new Map(users.map((user) => [user.id, user]));
|
|
178
178
|
|
|
179
179
|
const lines: string[] = [`Search: "${query}" — ${tweets.length} result${tweets.length !== 1 ? "s" : ""}`, ""];
|
|
180
180
|
tweets.forEach((tweet, i) => {
|
package/server/agent/prompt.ts
CHANGED
|
@@ -253,12 +253,17 @@ export function buildPluginPromptSections(role: Role): string[] {
|
|
|
253
253
|
// Some package plugins use an older gui-chat-protocol without the `prompt`
|
|
254
254
|
// field, so access it via `in` check to keep TypeScript happy.
|
|
255
255
|
const defPrompts = Object.fromEntries(
|
|
256
|
-
PLUGIN_DEFS.filter((
|
|
256
|
+
PLUGIN_DEFS.filter((definition) => "prompt" in definition && definition.prompt && allowedPlugins.has(definition.name)).map((definition) => [
|
|
257
|
+
definition.name,
|
|
258
|
+
(definition as unknown as { prompt: string }).prompt,
|
|
259
|
+
]),
|
|
257
260
|
);
|
|
258
261
|
|
|
259
262
|
// Collect prompts from MCP tools
|
|
260
263
|
const mcpToolPrompts = Object.fromEntries(
|
|
261
|
-
mcpTools
|
|
264
|
+
mcpTools
|
|
265
|
+
.filter((toolDef) => toolDef.prompt && allowedPlugins.has(toolDef.definition.name) && isMcpToolEnabled(toolDef))
|
|
266
|
+
.map((toolDef) => [toolDef.definition.name, toolDef.prompt as string]),
|
|
262
267
|
);
|
|
263
268
|
|
|
264
269
|
// MCP tool prompts override definition prompts if both exist
|
|
@@ -310,7 +315,7 @@ function buildInlinedHelpFiles(rolePrompt: string, workspacePath: string): strin
|
|
|
310
315
|
// Read() the stale legacy location.
|
|
311
316
|
return content ? `### ${WORKSPACE_DIRS.helps}/${name}\n\n${content}` : null;
|
|
312
317
|
})
|
|
313
|
-
.filter((
|
|
318
|
+
.filter((section): section is string => section !== null);
|
|
314
319
|
}
|
|
315
320
|
|
|
316
321
|
// Wrap a list of sub-entries under a single markdown heading, or
|
|
@@ -345,5 +350,5 @@ export function buildSystemPrompt(params: SystemPromptParams): string {
|
|
|
345
350
|
headingSection("Plugin Instructions", buildPluginPromptSections(role)),
|
|
346
351
|
];
|
|
347
352
|
|
|
348
|
-
return sections.filter((
|
|
353
|
+
return sections.filter((section): section is string => section !== null).join("\n\n");
|
|
349
354
|
}
|
|
@@ -48,7 +48,7 @@ export interface SandboxMountSpec {
|
|
|
48
48
|
*/
|
|
49
49
|
export function buildAllowedConfigMounts(home: string = homedir()): Record<string, SandboxMountSpec> {
|
|
50
50
|
return {
|
|
51
|
-
gh: {
|
|
51
|
+
["gh"]: {
|
|
52
52
|
name: "gh",
|
|
53
53
|
hostPath: path.join(home, ".config", "gh"),
|
|
54
54
|
containerPath: "/home/node/.config/gh",
|
|
@@ -261,7 +261,7 @@ export function resolveSandboxAuth(params: ResolveSandboxAuthParams): ResolvedSa
|
|
|
261
261
|
const args = [...configMountArgs(parsed.resolved), ...sshResult.args, ...sshAllowedHostsArgs, ...ghTokenArgs.args];
|
|
262
262
|
const allowedHostsSuffix = sshResult.args.length > 0 && params.sshAllowedHosts ? ` → hosts: ${params.sshAllowedHosts}` : "";
|
|
263
263
|
const appliedDescriptions = [
|
|
264
|
-
...parsed.resolved.map((
|
|
264
|
+
...parsed.resolved.map((spec) => `${spec.name} (${spec.description})`),
|
|
265
265
|
...(sshResult.args.length > 0 ? [`ssh-agent forward${allowedHostsSuffix}`] : []),
|
|
266
266
|
...(ghTokenArgs.args.length > 0 ? ["gh CLI (GH_TOKEN fallback)"] : []),
|
|
267
267
|
];
|
|
@@ -284,7 +284,7 @@ export function resolveSandboxAuth(params: ResolveSandboxAuthParams): ResolvedSa
|
|
|
284
284
|
// GH_TOKEN env var. This only runs when "gh" was explicitly
|
|
285
285
|
// requested (#259 opt-in principle).
|
|
286
286
|
function resolveGhTokenFallback(requestedNames: readonly string[], parsed: ParsedMountList): { args: string[] } {
|
|
287
|
-
const ghRequested = requestedNames.some((
|
|
287
|
+
const ghRequested = requestedNames.some((name) => name.trim() === "gh");
|
|
288
288
|
if (!ghRequested) return { args: [] };
|
|
289
289
|
|
|
290
290
|
// If an explicit GH_TOKEN is already in the environment, pass it.
|
|
@@ -295,8 +295,8 @@ function resolveGhTokenFallback(requestedNames: readonly string[], parsed: Parse
|
|
|
295
295
|
// If the file mount resolved (hosts.yml exists), the token might
|
|
296
296
|
// be in the file. Check if it's keyring-based by looking for
|
|
297
297
|
// "oauth_token" in the hosts.yml — if missing, fall back.
|
|
298
|
-
const ghResolved = parsed.resolved.some((
|
|
299
|
-
const ghMissing = parsed.missing.some((
|
|
298
|
+
const ghResolved = parsed.resolved.some((spec) => spec.name === "gh");
|
|
299
|
+
const ghMissing = parsed.missing.some((spec) => spec.name === "gh");
|
|
300
300
|
|
|
301
301
|
// gh dir doesn't exist at all → try extracting from keyring
|
|
302
302
|
// gh dir exists (mounted) → still try, since keyring auth leaves
|
|
@@ -324,6 +324,6 @@ function resolveGhTokenFallback(requestedNames: readonly string[], parsed: Parse
|
|
|
324
324
|
// Docker accepts POSIX-style paths even on Windows when using
|
|
325
325
|
// Docker Desktop, and the rest of the codebase already uses this
|
|
326
326
|
// helper in buildDockerSpawnArgs.
|
|
327
|
-
function toDockerPath(
|
|
328
|
-
return
|
|
327
|
+
function toDockerPath(hostPath: string): string {
|
|
328
|
+
return hostPath.replace(/\\/g, "/");
|
|
329
329
|
}
|
package/server/agent/stream.ts
CHANGED
|
@@ -98,7 +98,7 @@ function extractTextDelta(event: RawStreamEvent): string | null {
|
|
|
98
98
|
// Filter assistant block events: when deltas already streamed the
|
|
99
99
|
// text, remove text-type events to prevent duplication.
|
|
100
100
|
function filterAssistantBlocks(blockEvents: AgentEvent[], deltaStreamed: boolean): AgentEvent[] {
|
|
101
|
-
return deltaStreamed ? blockEvents.filter((
|
|
101
|
+
return deltaStreamed ? blockEvents.filter((agentEvent) => agentEvent.type !== EVENT_TYPES.text) : blockEvents;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// Stateful parser that deduplicates text across the three stages
|
|
@@ -151,11 +151,11 @@ export function createStreamParser(): {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
const content = event.message?.content;
|
|
154
|
-
const blockEvents = Array.isArray(content) ? content.map(blockToEvent).filter((
|
|
154
|
+
const blockEvents = Array.isArray(content) ? content.map(blockToEvent).filter((agentEvent): agentEvent is AgentEvent => agentEvent !== null) : [];
|
|
155
155
|
|
|
156
156
|
if (event.type === "assistant") {
|
|
157
157
|
const filtered = filterAssistantBlocks(blockEvents, textStreamedFromDeltas);
|
|
158
|
-
if (filtered.some((
|
|
158
|
+
if (filtered.some((agentEvent) => agentEvent.type === EVENT_TYPES.text)) {
|
|
159
159
|
textEmitted = true;
|
|
160
160
|
}
|
|
161
161
|
return [{ type: EVENT_TYPES.status, message: "Thinking..." }, ...filtered];
|
|
@@ -185,7 +185,7 @@ export function parseStreamEvent(event: RawStreamEvent): AgentEvent[] {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
const content = event.message?.content;
|
|
188
|
-
const blockEvents = Array.isArray(content) ? content.map(blockToEvent).filter((
|
|
188
|
+
const blockEvents = Array.isArray(content) ? content.map(blockToEvent).filter((agentEvent): agentEvent is AgentEvent => agentEvent !== null) : [];
|
|
189
189
|
|
|
190
190
|
if (event.type === "assistant") {
|
|
191
191
|
return [{ type: EVENT_TYPES.status, message: "Thinking..." }, ...blockEvents];
|
|
@@ -230,7 +230,7 @@ function resolveRefPath(prefixedPath: string): string | null {
|
|
|
230
230
|
const remainder = slashIdx >= 0 ? afterPrefix.slice(slashIdx + 1) : "";
|
|
231
231
|
|
|
232
232
|
const entries = getCachedReferenceDirs();
|
|
233
|
-
const entry = entries.find((
|
|
233
|
+
const entry = entries.find((refEntry) => refEntry.label === label);
|
|
234
234
|
if (!entry) return null;
|
|
235
235
|
|
|
236
236
|
let rootReal: string;
|
|
@@ -393,10 +393,10 @@ export async function buildTreeAsync(absPath: string, relPath: string, gitFilter
|
|
|
393
393
|
return buildTreeAsync(childAbs, childRel, localFilter);
|
|
394
394
|
});
|
|
395
395
|
const resolved = await Promise.all(childPromises);
|
|
396
|
-
const children = resolved.filter((
|
|
397
|
-
children.sort((
|
|
398
|
-
if (
|
|
399
|
-
return
|
|
396
|
+
const children = resolved.filter((childNode): childNode is TreeNode => childNode !== null);
|
|
397
|
+
children.sort((leftChild, rightChild) => {
|
|
398
|
+
if (leftChild.type !== rightChild.type) return leftChild.type === "dir" ? -1 : 1;
|
|
399
|
+
return leftChild.name.localeCompare(rightChild.name);
|
|
400
400
|
});
|
|
401
401
|
return {
|
|
402
402
|
name: relPath ? path.basename(relPath) : "",
|
|
@@ -460,10 +460,10 @@ export async function listDirShallow(absPath: string, relPath: string, gitFilter
|
|
|
460
460
|
};
|
|
461
461
|
});
|
|
462
462
|
const resolved = await Promise.all(childPromises);
|
|
463
|
-
const children = resolved.filter((
|
|
464
|
-
children.sort((
|
|
465
|
-
if (
|
|
466
|
-
return
|
|
463
|
+
const children = resolved.filter((childNode): childNode is TreeNode => childNode !== null);
|
|
464
|
+
children.sort((leftChild, rightChild) => {
|
|
465
|
+
if (leftChild.type !== rightChild.type) return leftChild.type === "dir" ? -1 : 1;
|
|
466
|
+
return leftChild.name.localeCompare(rightChild.name);
|
|
467
467
|
});
|
|
468
468
|
return {
|
|
469
469
|
name: relPath ? path.basename(relPath) : "",
|
|
@@ -93,11 +93,11 @@ async function handleTaskAction(action: string, input: Record<string, unknown>,
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
if (action === SCHEDULER_ACTIONS.deleteTask) {
|
|
96
|
-
const
|
|
96
|
+
const taskId = typeof input.id === "string" ? input.id : "";
|
|
97
97
|
const tasks = loadUserTasks();
|
|
98
|
-
const idx = tasks.findIndex((
|
|
98
|
+
const idx = tasks.findIndex((task) => task.id === taskId);
|
|
99
99
|
if (idx === -1) {
|
|
100
|
-
res.status(404).json({ error: `task not found: ${
|
|
100
|
+
res.status(404).json({ error: `task not found: ${taskId}` });
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
const name = tasks[idx].name;
|
|
@@ -107,17 +107,17 @@ async function handleTaskAction(action: string, input: Record<string, unknown>,
|
|
|
107
107
|
res.json({
|
|
108
108
|
uuid: crypto.randomUUID(),
|
|
109
109
|
message: `Task "${name}" deleted.`,
|
|
110
|
-
data: { deleted:
|
|
110
|
+
data: { deleted: taskId },
|
|
111
111
|
});
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
if (action === SCHEDULER_ACTIONS.runTask) {
|
|
116
|
-
const
|
|
116
|
+
const taskId = typeof input.id === "string" ? input.id : "";
|
|
117
117
|
const tasks = loadUserTasks();
|
|
118
|
-
const task = tasks.find((
|
|
118
|
+
const task = tasks.find((candidate) => candidate.id === taskId);
|
|
119
119
|
if (!task) {
|
|
120
|
-
res.status(404).json({ error: `task not found: ${
|
|
120
|
+
res.status(404).json({ error: `task not found: ${taskId}` });
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
const chatSessionId = crypto.randomUUID();
|
|
@@ -138,7 +138,7 @@ async function handleTaskAction(action: string, input: Record<string, unknown>,
|
|
|
138
138
|
res.json({
|
|
139
139
|
uuid: crypto.randomUUID(),
|
|
140
140
|
message: `Task "${task.name}" triggered.`,
|
|
141
|
-
data: { triggered:
|
|
141
|
+
data: { triggered: taskId, chatSessionId },
|
|
142
142
|
});
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
@@ -29,14 +29,14 @@ export type SchedulerActionResult =
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
export function sortItems(items: ScheduledItem[]): ScheduledItem[] {
|
|
32
|
-
return [...items].sort((
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
return
|
|
32
|
+
return [...items].sort((left, right) => {
|
|
33
|
+
const leftDate = typeof left.props.date === "string" ? left.props.date : null;
|
|
34
|
+
const rightDate = typeof right.props.date === "string" ? right.props.date : null;
|
|
35
|
+
const leftTime = typeof left.props.time === "string" ? left.props.time : "00:00";
|
|
36
|
+
const rightTime = typeof right.props.time === "string" ? right.props.time : "00:00";
|
|
37
|
+
const leftKey = leftDate ? `0_${leftDate}_${leftTime}` : `1_${left.createdAt}`;
|
|
38
|
+
const rightKey = rightDate ? `0_${rightDate}_${rightTime}` : `1_${right.createdAt}`;
|
|
39
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -84,11 +84,11 @@ export function handleDelete(items: ScheduledItem[], input: SchedulerActionInput
|
|
|
84
84
|
|
|
85
85
|
function applyPropPatch(current: ScheduledItem["props"], patch: Record<string, string | number | boolean | null>): ScheduledItem["props"] {
|
|
86
86
|
const next: ScheduledItem["props"] = { ...current };
|
|
87
|
-
for (const [
|
|
88
|
-
if (
|
|
89
|
-
delete next[
|
|
87
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
88
|
+
if (value === null) {
|
|
89
|
+
delete next[key];
|
|
90
90
|
} else {
|
|
91
|
-
next[
|
|
91
|
+
next[key] = value;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
return next;
|
|
@@ -29,7 +29,7 @@ router.get(API_ROUTES.scheduler.tasks, (_req: Request, res: Response) => {
|
|
|
29
29
|
// have no origin field of their own.
|
|
30
30
|
const systemTasks = getSchedulerTasks();
|
|
31
31
|
const userTasks = loadUserTasks();
|
|
32
|
-
const all = [...systemTasks.map((
|
|
32
|
+
const all = [...systemTasks.map((task) => ({ ...task, origin: "system" as const })), ...userTasks.map((task) => ({ ...task, origin: "user" as const }))];
|
|
33
33
|
res.json({ tasks: all });
|
|
34
34
|
});
|
|
35
35
|
|
|
@@ -58,14 +58,14 @@ router.post(API_ROUTES.scheduler.tasks, async (req: Request, res: Response) => {
|
|
|
58
58
|
// ── Update user task ────────────────────────────────────────────
|
|
59
59
|
|
|
60
60
|
router.put(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, res: Response) => {
|
|
61
|
-
const { id } = req.params;
|
|
61
|
+
const { id: taskId } = req.params;
|
|
62
62
|
try {
|
|
63
63
|
const updated = await withUserTaskLock(async (tasks) => {
|
|
64
|
-
const result = applyUpdate(tasks,
|
|
64
|
+
const result = applyUpdate(tasks, taskId, req.body);
|
|
65
65
|
if (result.kind === "error") {
|
|
66
66
|
throw new Error(result.error);
|
|
67
67
|
}
|
|
68
|
-
const task = result.tasks.find((
|
|
68
|
+
const task = result.tasks.find((taskItem) => taskItem.id === taskId);
|
|
69
69
|
return { tasks: result.tasks, result: task };
|
|
70
70
|
});
|
|
71
71
|
res.json({ task: updated });
|
|
@@ -83,15 +83,15 @@ router.put(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, res:
|
|
|
83
83
|
// ── Delete user task ────────────────────────────────────────────
|
|
84
84
|
|
|
85
85
|
router.delete(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, res: Response) => {
|
|
86
|
-
const { id } = req.params;
|
|
86
|
+
const { id: taskId } = req.params;
|
|
87
87
|
try {
|
|
88
88
|
await withUserTaskLock(async (tasks) => {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
const next = tasks.filter((
|
|
89
|
+
const index = tasks.findIndex((task) => task.id === taskId);
|
|
90
|
+
if (index === -1) throw new Error(`task not found: ${taskId}`);
|
|
91
|
+
const next = tasks.filter((task) => task.id !== taskId);
|
|
92
92
|
return { tasks: next, result: undefined };
|
|
93
93
|
});
|
|
94
|
-
res.json({ deleted:
|
|
94
|
+
res.json({ deleted: taskId });
|
|
95
95
|
} catch (err) {
|
|
96
96
|
const msg = errorMessage(err);
|
|
97
97
|
if (msg.startsWith("task not found")) {
|
|
@@ -106,10 +106,10 @@ router.delete(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, re
|
|
|
106
106
|
// ── Manual trigger ──────────────────────────────────────────────
|
|
107
107
|
|
|
108
108
|
router.post(API_ROUTES.scheduler.taskRun, async (req: Request<{ id: string }>, res: Response) => {
|
|
109
|
-
const { id } = req.params;
|
|
109
|
+
const { id: taskId } = req.params;
|
|
110
110
|
// Check user tasks first
|
|
111
111
|
const userTasks = loadUserTasks();
|
|
112
|
-
const userTask = userTasks.find((
|
|
112
|
+
const userTask = userTasks.find((task) => task.id === taskId);
|
|
113
113
|
if (userTask) {
|
|
114
114
|
const chatSessionId = crypto.randomUUID();
|
|
115
115
|
log.info("scheduler-tasks", "manual run (user task)", {
|
|
@@ -126,14 +126,14 @@ router.post(API_ROUTES.scheduler.taskRun, async (req: Request<{ id: string }>, r
|
|
|
126
126
|
error: String(err),
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
|
-
res.json({ triggered:
|
|
129
|
+
res.json({ triggered: taskId, chatSessionId });
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
// Not a user task — check system/skill tasks
|
|
133
133
|
const systemTasks = getSchedulerTasks();
|
|
134
|
-
const found = systemTasks.find((
|
|
134
|
+
const found = systemTasks.find((task) => task.id === taskId);
|
|
135
135
|
if (!found) {
|
|
136
|
-
notFound(res, `task not found: ${
|
|
136
|
+
notFound(res, `task not found: ${taskId}`);
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
// System tasks don't have a prompt to startChat with — return 400
|