Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 78 additions & 31 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { createReviewCheckpointManager } from "./review-checkpoints.js";
import { shutdownHttpServer } from "./server-shutdown.js";
import { formatPathForPrompt } from "./skills.js";
import { createWorkspaceStore } from "./workspace-store.js";
import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js";
import { formatAgentsPath, type LoadedAgentsFile, WorkspaceRegistry } from "./workspaces.js";
import { summarizeLocalAgentProfile } from "./local-agent-profiles.js";
import {
formatLocalAgentProviderAvailabilitySummary,
Expand Down Expand Up @@ -196,7 +196,7 @@ function serverInstructions(config: ServerConfig): string {
? `When ${toolNames.openWorkspace} returns available skills and a task matches a skill, use ${toolNames.read} to read that skill's path before proceeding. Skill paths may be outside the workspace, but ${toolNames.read} only permits advertised SKILL.md files and files under already-loaded skill directories. `
: "";

const agentsMd = `Follow instructions returned by ${toolNames.openWorkspace}. Before working under a path listed in availableAgentsFiles, use ${toolNames.read} to inspect that instruction file and follow it. `;
const agentsMd = `Follow instructions returned by ${toolNames.openWorkspace}. DevSpace returns newly encountered AGENTS.md or CLAUDE.md files when a later path-aware tool enters their directory; follow those instructions before continuing. `;

return `Use DevSpace as a local coding workspace. Call ${toolNames.openWorkspace} once per project folder or worktree to obtain a workspaceId. Reuse that same workspaceId for all later file, search, edit, write, show-changes, and shell tools in that folder; do not call ${toolNames.openWorkspace} again unless switching folders/worktrees, changing checkout/worktree mode, the workspaceId is rejected as unknown, or the user explicitly asks to reopen. ${agentsMd}${skills}${inspection}Prefer ${toolNames.edit} for targeted modifications, ${toolNames.write} only for new files or complete rewrites, and ${toolNames.shell} for tests, builds, git inspection, package scripts, and commands that are better executed by the shell. Do not create or modify files with ${toolNames.shell}; avoid shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or any command whose purpose is to write project files.${showChangesInstruction}`;
}
Expand Down Expand Up @@ -344,6 +344,15 @@ function textBlock(text: string): ToolContent {
return { type: "text", text };
}

function instructionContent(
agentsFiles: LoadedAgentsFile[],
workspaceRoot: string,
): ToolContent[] {
return agentsFiles.map((file) => textBlock(
`Loaded project instructions from ${formatAgentsPath(file.path, workspaceRoot)}:\n${file.content}`,
));
}

function textSummary(content: ToolContent[]): {
lines: number;
characters: number;
Expand Down Expand Up @@ -512,9 +521,10 @@ function processToolResponse(
workspaceId: string,
snapshot: ProcessSnapshot,
summary: Record<string, unknown>,
additionalContent: ToolContent[] = [],
) {
const result = processResult(snapshot);
const content = [textBlock(result)];
const content = [textBlock(result), ...additionalContent];
const outputSummary = textSummary(snapshot.output ? [textBlock(snapshot.output)] : []);
return {
content,
Expand All @@ -527,7 +537,7 @@ function processToolResponse(
},
},
structuredContent: {
result,
result: contentText(content),
sessionId: snapshot.sessionId,
running: snapshot.running,
exitCode: snapshot.exitCode,
Expand Down Expand Up @@ -587,6 +597,11 @@ function registerCodexProcessTools(
const startedAt = performance.now();
const workspace = workspaces.getWorkspace(workspaceId);
const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
const agentsFiles = await workspaces.loadAgentsFilesForPath(
workspace,
cwd,
"directory",
);
const snapshot = await processSessions.start({
workspaceId,
command: cmd,
Expand Down Expand Up @@ -615,7 +630,7 @@ function registerCodexProcessTools(
running: snapshot.running,
exitCode: snapshot.exitCode,
wallTimeMs: snapshot.wallTimeMs,
});
}, instructionContent(agentsFiles, workspace.root));
},
);

Expand Down Expand Up @@ -739,7 +754,7 @@ function createMcpServer(
{
title: "Open workspace",
description:
"Open a local project directory as a coding workspace. Call this once per project folder or worktree before reading, editing, searching, writing, showing changes, or running commands. Reuse the returned workspaceId for later calls in the same folder; do not call open_workspace again unless switching folders/worktrees, changing checkout/worktree mode, the workspaceId is rejected as unknown, or the user explicitly asks to reopen. By default this opens the actual checkout; set mode=\"worktree\" when the user asks for an isolated or parallel coding session. Returns a workspaceId, loaded root project instructions, and nested instruction file paths the model should read before working in those directories.",
"Open a local project directory as a coding workspace. Call this once per project folder or worktree before reading, editing, searching, writing, showing changes, or running commands. Reuse the returned workspaceId for later calls in the same folder; do not call open_workspace again unless switching folders/worktrees, changing checkout/worktree mode, the workspaceId is rejected as unknown, or the user explicitly asks to reopen. By default this opens the actual checkout; set mode=\"worktree\" when the user asks for an isolated or parallel coding session. Returns a workspaceId and root project instructions. DevSpace returns nested instructions when a later tool enters their directory.",
inputSchema: {
path: z
.string()
Expand Down Expand Up @@ -817,8 +832,8 @@ function createMcpServer(
path: formatAgentsPath(file.path, workspace.root),
}));
const instruction = config.skillsEnabled
? "Use this workspaceId in all subsequent tool calls for this project. Do not call open_workspace again for this same folder unless this workspaceId stops working, the user asks to reopen, or you switch to a different folder/worktree. Follow loaded agentsFiles instructions. Before working under a path listed in availableAgentsFiles, read that instruction file. When a task matches an available skill in skills, read its path before proceeding."
: "Use this workspaceId in all subsequent tool calls for this project. Do not call open_workspace again for this same folder unless this workspaceId stops working, the user asks to reopen, or you switch to a different folder/worktree. Follow loaded agentsFiles instructions. Before working under a path listed in availableAgentsFiles, read that instruction file.";
? "Use this workspaceId in all subsequent tool calls for this project. Do not call open_workspace again for this same folder unless this workspaceId stops working, the user asks to reopen, or you switch to a different folder/worktree. Follow loaded agentsFiles instructions and any nested instructions returned by later path-aware tool calls. When a task matches an available skill in skills, read its path before proceeding."
: "Use this workspaceId in all subsequent tool calls for this project. Do not call open_workspace again for this same folder unless this workspaceId stops working, the user asks to reopen, or you switch to a different folder/worktree. Follow loaded agentsFiles instructions and any nested instructions returned by later path-aware tool calls.";
const resultContent: ToolContent[] = [
{
type: "text" as const,
Expand Down Expand Up @@ -940,6 +955,9 @@ function createMcpServer(
const startedAt = performance.now();
const workspace = workspaces.getWorkspace(workspaceId);
const readPath = workspaces.resolveReadPath(workspace, input.path);
const agentsFiles = readPath.skillRead
? []
: await workspaces.loadAgentsFilesForPath(workspace, readPath.absolutePath);
const response = await readFileTool(
{ ...input, path: readPath.absolutePath },
{
Expand All @@ -958,9 +976,10 @@ function createMcpServer(
return response;
}
workspaces.markReadPathLoaded(workspace, readPath);
const content = [...response.content, ...instructionContent(agentsFiles, workspace.root)];

const summary = {
...textSummary(response.content),
...textSummary(content),
offset: input.offset ?? 1,
limited: input.limit !== undefined,
};
Expand All @@ -974,17 +993,18 @@ function createMcpServer(

return {
...response,
content,
_meta: {
tool: toolNames.read,
card: {
workspaceId,
path: input.path,
summary,
payload: { content: response.content },
payload: { content },
},
},
structuredContent: {
result: contentText(response.content),
result: contentText(content),
},
};
},
Expand Down Expand Up @@ -1014,7 +1034,8 @@ function createMcpServer(
async ({ workspaceId, ...input }) => {
const startedAt = performance.now();
const workspace = workspaces.getWorkspace(workspaceId);
workspaces.resolvePath(workspace, input.path);
const path = workspaces.resolvePath(workspace, input.path);
const agentsFiles = await workspaces.loadAgentsFilesForPath(workspace, path);
const response = await writeFileTool(input, {
cwd: workspace.root,
root: workspace.root,
Expand All @@ -1030,6 +1051,7 @@ function createMcpServer(
}

const patch = newFilePatch(input.path, input.content);
const content = [...response.content, ...instructionContent(agentsFiles, workspace.root)];
const stats = countDiffStats(patch);
const summary = {
...stats,
Expand All @@ -1046,20 +1068,21 @@ function createMcpServer(

return {
...response,
content,
_meta: {
tool: toolNames.write,
card: {
workspaceId,
path: input.path,
summary,
payload: {
content: response.content,
content,
patch,
},
},
},
structuredContent: {
result: contentText(response.content),
result: contentText(content),
},
};
},
Expand Down Expand Up @@ -1101,7 +1124,8 @@ function createMcpServer(
async ({ workspaceId, ...input }) => {
const startedAt = performance.now();
const workspace = workspaces.getWorkspace(workspaceId);
workspaces.resolvePath(workspace, input.path);
const path = workspaces.resolvePath(workspace, input.path);
const agentsFiles = await workspaces.loadAgentsFilesForPath(workspace, path);
const response = await editFileTool(input, {
cwd: workspace.root,
root: workspace.root,
Expand All @@ -1124,7 +1148,10 @@ function createMcpServer(
editCount: input.edits.length,
};
const editResultText = `Edited ${input.path} (+${stats.additions} -${stats.removals}).`;
const editContent = [textBlock(editResultText)];
const editContent = [
textBlock(editResultText),
...instructionContent(agentsFiles, workspace.root),
];
logToolCall(config, {
tool: toolNames.edit,
workspaceId,
Expand Down Expand Up @@ -1315,7 +1342,10 @@ function createMcpServer(
async ({ workspaceId, ...input }) => {
const startedAt = performance.now();
const workspace = workspaces.getWorkspace(workspaceId);
if (input.path) workspaces.resolvePath(workspace, input.path);
const path = input.path
? workspaces.resolvePath(workspace, input.path)
: workspace.root;
const agentsFiles = await workspaces.loadAgentsFilesForPath(workspace, path);
const response = await grepFilesTool(input, {
cwd: workspace.root,
root: workspace.root,
Expand All @@ -1330,10 +1360,11 @@ function createMcpServer(
return response;
}

const content = [...response.content, ...instructionContent(agentsFiles, workspace.root)];
const summary = {
pattern: input.pattern,
scope: input.path ?? ".",
...textSummary(response.content),
...textSummary(content),
};
logToolCall(config, {
tool: toolNames.grep,
Expand All @@ -1345,17 +1376,18 @@ function createMcpServer(

return {
...response,
content,
_meta: {
tool: toolNames.grep,
card: {
workspaceId,
path: input.path,
summary,
payload: { content: response.content },
payload: { content },
},
},
structuredContent: {
result: contentText(response.content),
result: contentText(content),
},
};
},
Expand Down Expand Up @@ -1385,7 +1417,10 @@ function createMcpServer(
async ({ workspaceId, ...input }) => {
const startedAt = performance.now();
const workspace = workspaces.getWorkspace(workspaceId);
if (input.path) workspaces.resolvePath(workspace, input.path);
const path = input.path
? workspaces.resolvePath(workspace, input.path)
: workspace.root;
const agentsFiles = await workspaces.loadAgentsFilesForPath(workspace, path);
const response = await findFilesTool(input, {
cwd: workspace.root,
root: workspace.root,
Expand All @@ -1400,10 +1435,11 @@ function createMcpServer(
return response;
}

const content = [...response.content, ...instructionContent(agentsFiles, workspace.root)];
const summary = {
pattern: input.pattern,
scope: input.path ?? ".",
...textSummary(response.content),
...textSummary(content),
};
logToolCall(config, {
tool: toolNames.glob,
Expand All @@ -1415,17 +1451,18 @@ function createMcpServer(

return {
...response,
content,
_meta: {
tool: toolNames.glob,
card: {
workspaceId,
path: input.path,
summary,
payload: { content: response.content },
payload: { content },
},
},
structuredContent: {
result: contentText(response.content),
result: contentText(content),
},
};
},
Expand Down Expand Up @@ -1455,7 +1492,8 @@ function createMcpServer(
async ({ workspaceId, ...input }) => {
const startedAt = performance.now();
const workspace = workspaces.getWorkspace(workspaceId);
workspaces.resolvePath(workspace, input.path);
const path = workspaces.resolvePath(workspace, input.path);
const agentsFiles = await workspaces.loadAgentsFilesForPath(workspace, path);
const response = await listDirectoryTool(input, {
cwd: workspace.root,
root: workspace.root,
Expand All @@ -1470,7 +1508,8 @@ function createMcpServer(
return response;
}

const summary = textSummary(response.content);
const content = [...response.content, ...instructionContent(agentsFiles, workspace.root)];
const summary = textSummary(content);
logToolCall(config, {
tool: toolNames.ls,
workspaceId,
Expand All @@ -1481,17 +1520,18 @@ function createMcpServer(

return {
...response,
content,
_meta: {
tool: toolNames.ls,
card: {
workspaceId,
path: input.path,
summary,
payload: { content: response.content },
payload: { content },
},
},
structuredContent: {
result: contentText(response.content),
result: contentText(content),
},
};
},
Expand Down Expand Up @@ -1540,6 +1580,11 @@ function createMcpServer(
workspace,
workingDirectory,
);
const agentsFiles = await workspaces.loadAgentsFilesForPath(
workspace,
cwd,
"directory",
);
const response = await runShellTool(input, {
cwd,
root: workspace.root,
Expand All @@ -1556,10 +1601,11 @@ function createMcpServer(
return response;
}

const content = [...response.content, ...instructionContent(agentsFiles, workspace.root)];
const summary = {
command: input.command,
workingDirectory: workingDirectory ?? ".",
...textSummary(response.content),
...textSummary(content),
};
logToolCall(config, {
tool: toolNames.shell,
Expand All @@ -1573,17 +1619,18 @@ function createMcpServer(

return {
...response,
content,
_meta: {
tool: toolNames.shell,
card: {
workspaceId,
path: workingDirectory,
summary,
payload: { content: response.content },
payload: { content },
},
},
structuredContent: {
result: contentText(response.content),
result: contentText(content),
},
};
},
Expand Down
Loading
Loading