fixing imports
This commit is contained in:
@@ -16,10 +16,19 @@ import type {
|
||||
AgentTier,
|
||||
AgentColor,
|
||||
} from "@/types/agent-definition";
|
||||
import { DEFAULT_AGENT_DEFINITION, AGENT_DEFINITION_SCHEMA } from "@/types/agent-definition";
|
||||
import { AGENT_DEFINITION, AGENT_DEFINITION_PATHS, AGENT_MESSAGES } from "@constants/agent-definition";
|
||||
import {
|
||||
DEFAULT_AGENT_DEFINITION,
|
||||
AGENT_DEFINITION_SCHEMA,
|
||||
} from "@/types/agent-definition";
|
||||
import {
|
||||
AGENT_DEFINITION,
|
||||
AGENT_DEFINITION_PATHS,
|
||||
AGENT_MESSAGES,
|
||||
} from "@constants/agent-definition";
|
||||
|
||||
const parseFrontmatter = (content: string): { frontmatter: Record<string, unknown>; body: string } | null => {
|
||||
const parseFrontmatter = (
|
||||
content: string,
|
||||
): { frontmatter: Record<string, unknown>; body: string } | null => {
|
||||
const delimiter = AGENT_DEFINITION.FRONTMATTER_DELIMITER;
|
||||
const lines = content.split("\n");
|
||||
|
||||
@@ -27,13 +36,18 @@ const parseFrontmatter = (content: string): { frontmatter: Record<string, unknow
|
||||
return null;
|
||||
}
|
||||
|
||||
const endIndex = lines.findIndex((line, index) => index > 0 && line.trim() === delimiter);
|
||||
const endIndex = lines.findIndex(
|
||||
(line, index) => index > 0 && line.trim() === delimiter,
|
||||
);
|
||||
if (endIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const frontmatterLines = lines.slice(1, endIndex);
|
||||
const body = lines.slice(endIndex + 1).join("\n").trim();
|
||||
const body = lines
|
||||
.slice(endIndex + 1)
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
// Simple YAML parser for frontmatter
|
||||
const frontmatter: Record<string, unknown> = {};
|
||||
@@ -85,7 +99,9 @@ const parseFrontmatter = (content: string): { frontmatter: Record<string, unknow
|
||||
return { frontmatter, body };
|
||||
};
|
||||
|
||||
const validateFrontmatter = (frontmatter: Record<string, unknown>): AgentFrontmatter | null => {
|
||||
const validateFrontmatter = (
|
||||
frontmatter: Record<string, unknown>,
|
||||
): AgentFrontmatter | null => {
|
||||
const { required } = AGENT_DEFINITION_SCHEMA;
|
||||
|
||||
for (const field of required) {
|
||||
@@ -98,7 +114,11 @@ const validateFrontmatter = (frontmatter: Record<string, unknown>): AgentFrontma
|
||||
const description = frontmatter.description;
|
||||
const tools = frontmatter.tools;
|
||||
|
||||
if (typeof name !== "string" || typeof description !== "string" || !Array.isArray(tools)) {
|
||||
if (
|
||||
typeof name !== "string" ||
|
||||
typeof description !== "string" ||
|
||||
!Array.isArray(tools)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -108,7 +128,8 @@ const validateFrontmatter = (frontmatter: Record<string, unknown>): AgentFrontma
|
||||
tools: tools as ReadonlyArray<string>,
|
||||
tier: (frontmatter.tier as AgentTier) || DEFAULT_AGENT_DEFINITION.tier,
|
||||
color: (frontmatter.color as AgentColor) || DEFAULT_AGENT_DEFINITION.color,
|
||||
maxTurns: (frontmatter.maxTurns as number) || DEFAULT_AGENT_DEFINITION.maxTurns,
|
||||
maxTurns:
|
||||
(frontmatter.maxTurns as number) || DEFAULT_AGENT_DEFINITION.maxTurns,
|
||||
triggerPhrases: (frontmatter.triggerPhrases as ReadonlyArray<string>) || [],
|
||||
capabilities: (frontmatter.capabilities as ReadonlyArray<string>) || [],
|
||||
allowedPaths: frontmatter.allowedPaths as ReadonlyArray<string> | undefined,
|
||||
@@ -116,7 +137,10 @@ const validateFrontmatter = (frontmatter: Record<string, unknown>): AgentFrontma
|
||||
};
|
||||
};
|
||||
|
||||
const frontmatterToDefinition = (frontmatter: AgentFrontmatter, content: string): AgentDefinition => ({
|
||||
const frontmatterToDefinition = (
|
||||
frontmatter: AgentFrontmatter,
|
||||
content: string,
|
||||
): AgentDefinition => ({
|
||||
name: frontmatter.name,
|
||||
description: frontmatter.description,
|
||||
tools: frontmatter.tools,
|
||||
@@ -132,19 +156,29 @@ const frontmatterToDefinition = (frontmatter: AgentFrontmatter, content: string)
|
||||
},
|
||||
});
|
||||
|
||||
export const loadAgentDefinitionFile = async (filePath: string): Promise<AgentLoadResult> => {
|
||||
export const loadAgentDefinitionFile = async (
|
||||
filePath: string,
|
||||
): Promise<AgentLoadResult> => {
|
||||
try {
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
if (!parsed) {
|
||||
return { success: false, error: AGENT_MESSAGES.INVALID_FRONTMATTER, filePath };
|
||||
return {
|
||||
success: false,
|
||||
error: AGENT_MESSAGES.INVALID_FRONTMATTER,
|
||||
filePath,
|
||||
};
|
||||
}
|
||||
|
||||
const frontmatter = validateFrontmatter(parsed.frontmatter);
|
||||
|
||||
if (!frontmatter) {
|
||||
return { success: false, error: AGENT_MESSAGES.MISSING_REQUIRED, filePath };
|
||||
return {
|
||||
success: false,
|
||||
error: AGENT_MESSAGES.MISSING_REQUIRED,
|
||||
filePath,
|
||||
};
|
||||
}
|
||||
|
||||
const agent = frontmatterToDefinition(frontmatter, parsed.body);
|
||||
@@ -157,7 +191,7 @@ export const loadAgentDefinitionFile = async (filePath: string): Promise<AgentLo
|
||||
};
|
||||
|
||||
export const loadAgentDefinitionsFromDirectory = async (
|
||||
directoryPath: string
|
||||
directoryPath: string,
|
||||
): Promise<ReadonlyArray<AgentLoadResult>> => {
|
||||
const resolvedPath = directoryPath.replace("~", homedir());
|
||||
|
||||
@@ -168,11 +202,11 @@ export const loadAgentDefinitionsFromDirectory = async (
|
||||
try {
|
||||
const files = await readdir(resolvedPath);
|
||||
const mdFiles = files.filter(
|
||||
(file) => extname(file) === AGENT_DEFINITION.FILE_EXTENSION
|
||||
(file) => extname(file) === AGENT_DEFINITION.FILE_EXTENSION,
|
||||
);
|
||||
|
||||
const results = await Promise.all(
|
||||
mdFiles.map((file) => loadAgentDefinitionFile(join(resolvedPath, file)))
|
||||
mdFiles.map((file) => loadAgentDefinitionFile(join(resolvedPath, file))),
|
||||
);
|
||||
|
||||
return results;
|
||||
@@ -182,7 +216,7 @@ export const loadAgentDefinitionsFromDirectory = async (
|
||||
};
|
||||
|
||||
export const loadAllAgentDefinitions = async (
|
||||
projectPath: string
|
||||
projectPath: string,
|
||||
): Promise<AgentRegistry> => {
|
||||
const agents = new Map<string, AgentDefinition>();
|
||||
const byTrigger = new Map<string, string>();
|
||||
@@ -225,7 +259,7 @@ export const loadAllAgentDefinitions = async (
|
||||
|
||||
export const findAgentByTrigger = (
|
||||
registry: AgentRegistry,
|
||||
text: string
|
||||
text: string,
|
||||
): AgentDefinition | undefined => {
|
||||
const normalized = text.toLowerCase();
|
||||
|
||||
@@ -240,23 +274,28 @@ export const findAgentByTrigger = (
|
||||
|
||||
export const findAgentsByCapability = (
|
||||
registry: AgentRegistry,
|
||||
capability: string
|
||||
capability: string,
|
||||
): ReadonlyArray<AgentDefinition> => {
|
||||
const agentNames = registry.byCapability.get(capability) || [];
|
||||
return agentNames
|
||||
.map((name: string) => registry.agents.get(name))
|
||||
.filter((a: AgentDefinition | undefined): a is AgentDefinition => a !== undefined);
|
||||
.filter(
|
||||
(a: AgentDefinition | undefined): a is AgentDefinition => a !== undefined,
|
||||
);
|
||||
};
|
||||
|
||||
export const getAgentByName = (
|
||||
registry: AgentRegistry,
|
||||
name: string
|
||||
name: string,
|
||||
): AgentDefinition | undefined => registry.agents.get(name);
|
||||
|
||||
export const listAllAgents = (registry: AgentRegistry): ReadonlyArray<AgentDefinition> =>
|
||||
Array.from(registry.agents.values());
|
||||
export const listAllAgents = (
|
||||
registry: AgentRegistry,
|
||||
): ReadonlyArray<AgentDefinition> => Array.from(registry.agents.values());
|
||||
|
||||
export const createAgentDefinitionContent = (agent: AgentDefinition): string => {
|
||||
export const createAgentDefinitionContent = (
|
||||
agent: AgentDefinition,
|
||||
): string => {
|
||||
const frontmatter = [
|
||||
"---",
|
||||
`name: ${agent.name}`,
|
||||
@@ -272,7 +311,9 @@ export const createAgentDefinitionContent = (agent: AgentDefinition): string =>
|
||||
|
||||
if (agent.triggerPhrases && agent.triggerPhrases.length > 0) {
|
||||
frontmatter.push("triggerPhrases:");
|
||||
agent.triggerPhrases.forEach((phrase: string) => frontmatter.push(` - ${phrase}`));
|
||||
agent.triggerPhrases.forEach((phrase: string) =>
|
||||
frontmatter.push(` - ${phrase}`),
|
||||
);
|
||||
}
|
||||
|
||||
if (agent.capabilities && agent.capabilities.length > 0) {
|
||||
@@ -282,7 +323,8 @@ export const createAgentDefinitionContent = (agent: AgentDefinition): string =>
|
||||
|
||||
frontmatter.push("---");
|
||||
|
||||
const content = agent.systemPrompt || `# ${agent.name}\n\n${agent.description}`;
|
||||
const content =
|
||||
agent.systemPrompt || `# ${agent.name}\n\n${agent.description}`;
|
||||
|
||||
return `${frontmatter.join("\n")}\n\n${content}`;
|
||||
};
|
||||
|
||||
@@ -88,7 +88,8 @@ const processStreamChunk = (
|
||||
|
||||
// OpenAI streaming format includes index in each chunk
|
||||
// Use index from chunk if available, otherwise find by id or default to 0
|
||||
const chunkIndex = tc.index ?? (tc.id ? getToolCallIndex(tc.id, accumulator) : 0);
|
||||
const chunkIndex =
|
||||
tc.index ?? (tc.id ? getToolCallIndex(tc.id, accumulator) : 0);
|
||||
|
||||
// Get or create partial tool call
|
||||
let partial = accumulator.toolCalls.get(chunkIndex);
|
||||
|
||||
@@ -332,7 +332,10 @@ export const connect = async (): Promise<boolean> => {
|
||||
|
||||
// Try to get stats to verify credentials are valid
|
||||
const projectId = brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID;
|
||||
const statsResponse = await brainApi.getKnowledgeStats(projectId, authToken);
|
||||
const statsResponse = await brainApi.getKnowledgeStats(
|
||||
projectId,
|
||||
authToken,
|
||||
);
|
||||
|
||||
if (statsResponse.success && statsResponse.data) {
|
||||
updateState({
|
||||
@@ -539,7 +542,9 @@ export const extractAndLearn = async (
|
||||
if (response.success) {
|
||||
// Update knowledge count
|
||||
const newCount =
|
||||
brainState.knowledgeCount + response.data.stored + response.data.updated;
|
||||
brainState.knowledgeCount +
|
||||
response.data.stored +
|
||||
response.data.updated;
|
||||
updateState({ knowledgeCount: newCount });
|
||||
return response;
|
||||
}
|
||||
@@ -563,7 +568,9 @@ export const extractAndLearn = async (
|
||||
export const searchMemories = async (
|
||||
query: string,
|
||||
limit = 10,
|
||||
): Promise<{ memories: Array<{ content: string; similarity: number }> } | null> => {
|
||||
): Promise<{
|
||||
memories: Array<{ content: string; similarity: number }>;
|
||||
} | null> => {
|
||||
if (!isConnected()) {
|
||||
return null;
|
||||
}
|
||||
@@ -599,7 +606,12 @@ export const searchMemories = async (
|
||||
*/
|
||||
export const storeMemory = async (
|
||||
content: string,
|
||||
type: "fact" | "pattern" | "correction" | "preference" | "context" = "context",
|
||||
type:
|
||||
| "fact"
|
||||
| "pattern"
|
||||
| "correction"
|
||||
| "preference"
|
||||
| "context" = "context",
|
||||
): Promise<boolean> => {
|
||||
if (!isConnected()) {
|
||||
return false;
|
||||
|
||||
@@ -448,9 +448,7 @@ const pullFromCloud = async (
|
||||
/**
|
||||
* Check if pulled item conflicts with local changes
|
||||
*/
|
||||
const checkLocalConflict = async (
|
||||
_item: SyncItem,
|
||||
): Promise<boolean> => {
|
||||
const checkLocalConflict = async (_item: SyncItem): Promise<boolean> => {
|
||||
// Check if we have pending changes for this item
|
||||
const queued = await hasQueuedItems();
|
||||
return queued;
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
* Handles sync conflicts between local and remote brain data.
|
||||
*/
|
||||
|
||||
import {
|
||||
CONFLICT_LABELS,
|
||||
} from "@constants/brain-cloud";
|
||||
import { CONFLICT_LABELS } from "@constants/brain-cloud";
|
||||
import type {
|
||||
SyncConflict,
|
||||
ConflictStrategy,
|
||||
@@ -83,21 +81,22 @@ export const resolveAllConflicts = (
|
||||
/**
|
||||
* Conflict resolution strategies
|
||||
*/
|
||||
const resolvers: Record<ConflictStrategy, (conflict: SyncConflict) => unknown> = {
|
||||
"local-wins": (conflict) => conflict.localData,
|
||||
const resolvers: Record<ConflictStrategy, (conflict: SyncConflict) => unknown> =
|
||||
{
|
||||
"local-wins": (conflict) => conflict.localData,
|
||||
|
||||
"remote-wins": (conflict) => conflict.remoteData,
|
||||
"remote-wins": (conflict) => conflict.remoteData,
|
||||
|
||||
manual: (_conflict) => {
|
||||
// Manual resolution returns null - requires user input
|
||||
return null;
|
||||
},
|
||||
manual: (_conflict) => {
|
||||
// Manual resolution returns null - requires user input
|
||||
return null;
|
||||
},
|
||||
|
||||
merge: (conflict) => {
|
||||
// Attempt to merge the data
|
||||
return mergeData(conflict.localData, conflict.remoteData);
|
||||
},
|
||||
};
|
||||
merge: (conflict) => {
|
||||
// Attempt to merge the data
|
||||
return mergeData(conflict.localData, conflict.remoteData);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to merge two data objects
|
||||
@@ -119,7 +118,9 @@ const mergeData = (local: unknown, remote: unknown): unknown => {
|
||||
|
||||
// Use most recent timestamp
|
||||
const localTime = (localObj.updatedAt ?? localObj.timestamp ?? 0) as number;
|
||||
const remoteTime = (remoteObj.updatedAt ?? remoteObj.timestamp ?? 0) as number;
|
||||
const remoteTime = (remoteObj.updatedAt ??
|
||||
remoteObj.timestamp ??
|
||||
0) as number;
|
||||
merged.updatedAt = Math.max(localTime, remoteTime);
|
||||
|
||||
return merged;
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
* Exposes Brain as an MCP server for external tools
|
||||
*/
|
||||
|
||||
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import {
|
||||
createServer,
|
||||
type Server,
|
||||
type IncomingMessage,
|
||||
type ServerResponse,
|
||||
} from "node:http";
|
||||
|
||||
import type {
|
||||
BrainMcpServerConfig,
|
||||
@@ -20,16 +25,26 @@ import {
|
||||
BRAIN_MCP_TOOLS,
|
||||
MCP_ERROR_CODES,
|
||||
} from "@/types/brain-mcp";
|
||||
import {
|
||||
BRAIN_MCP_MESSAGES,
|
||||
BRAIN_MCP_ERRORS,
|
||||
} from "@constants/brain-mcp";
|
||||
import { BRAIN_MCP_MESSAGES, BRAIN_MCP_ERRORS } from "@constants/brain-mcp";
|
||||
|
||||
type BrainService = {
|
||||
recall: (query: string, limit?: number) => Promise<unknown>;
|
||||
learn: (name: string, whatItDoes: string, options?: unknown) => Promise<unknown>;
|
||||
searchMemories: (query: string, limit?: number, type?: string) => Promise<unknown>;
|
||||
relate: (source: string, target: string, type: string, weight?: number) => Promise<unknown>;
|
||||
learn: (
|
||||
name: string,
|
||||
whatItDoes: string,
|
||||
options?: unknown,
|
||||
) => Promise<unknown>;
|
||||
searchMemories: (
|
||||
query: string,
|
||||
limit?: number,
|
||||
type?: string,
|
||||
) => Promise<unknown>;
|
||||
relate: (
|
||||
source: string,
|
||||
target: string,
|
||||
type: string,
|
||||
weight?: number,
|
||||
) => Promise<unknown>;
|
||||
getContext: (query: string, maxConcepts?: number) => Promise<string>;
|
||||
getStats: () => Promise<unknown>;
|
||||
isConnected: () => boolean;
|
||||
@@ -59,7 +74,11 @@ const state: McpServerState = {
|
||||
apiKeys: new Set(),
|
||||
};
|
||||
|
||||
const createMcpError = (code: number, message: string, data?: unknown): McpError => ({
|
||||
const createMcpError = (
|
||||
code: number,
|
||||
message: string,
|
||||
data?: unknown,
|
||||
): McpError => ({
|
||||
code,
|
||||
message,
|
||||
data,
|
||||
@@ -68,7 +87,7 @@ const createMcpError = (code: number, message: string, data?: unknown): McpError
|
||||
const createMcpResponse = (
|
||||
id: string | number,
|
||||
content?: ReadonlyArray<McpContent>,
|
||||
error?: McpError
|
||||
error?: McpError,
|
||||
): BrainMcpResponse => {
|
||||
if (error) {
|
||||
return { id, error };
|
||||
@@ -111,7 +130,9 @@ const checkRateLimit = (clientIp: string): boolean => {
|
||||
const validateApiKey = (req: IncomingMessage): boolean => {
|
||||
if (!state.config.enableAuth) return true;
|
||||
|
||||
const apiKey = req.headers[state.config.apiKeyHeader.toLowerCase()] as string | undefined;
|
||||
const apiKey = req.headers[state.config.apiKeyHeader.toLowerCase()] as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (!apiKey) return false;
|
||||
|
||||
@@ -123,45 +144,62 @@ const validateApiKey = (req: IncomingMessage): boolean => {
|
||||
|
||||
const handleToolCall = async (
|
||||
toolName: BrainMcpToolName,
|
||||
args: Record<string, unknown>
|
||||
args: Record<string, unknown>,
|
||||
): Promise<McpContent[]> => {
|
||||
if (!state.brainService) {
|
||||
throw createMcpError(MCP_ERROR_CODES.BRAIN_UNAVAILABLE, BRAIN_MCP_MESSAGES.SERVER_NOT_RUNNING);
|
||||
throw createMcpError(
|
||||
MCP_ERROR_CODES.BRAIN_UNAVAILABLE,
|
||||
BRAIN_MCP_MESSAGES.SERVER_NOT_RUNNING,
|
||||
);
|
||||
}
|
||||
|
||||
if (!state.brainService.isConnected()) {
|
||||
throw createMcpError(MCP_ERROR_CODES.BRAIN_UNAVAILABLE, "Brain service not connected");
|
||||
throw createMcpError(
|
||||
MCP_ERROR_CODES.BRAIN_UNAVAILABLE,
|
||||
"Brain service not connected",
|
||||
);
|
||||
}
|
||||
|
||||
const tool = BRAIN_MCP_TOOLS.find((t: BrainMcpTool) => t.name === toolName);
|
||||
if (!tool) {
|
||||
throw createMcpError(MCP_ERROR_CODES.TOOL_NOT_FOUND, `Tool not found: ${toolName}`);
|
||||
throw createMcpError(
|
||||
MCP_ERROR_CODES.TOOL_NOT_FOUND,
|
||||
`Tool not found: ${toolName}`,
|
||||
);
|
||||
}
|
||||
|
||||
let result: unknown;
|
||||
|
||||
const toolHandlers: Record<BrainMcpToolName, () => Promise<unknown>> = {
|
||||
brain_recall: () => state.brainService!.recall(args.query as string, args.limit as number | undefined),
|
||||
brain_learn: () => state.brainService!.learn(
|
||||
args.name as string,
|
||||
args.whatItDoes as string,
|
||||
{ keywords: args.keywords, patterns: args.patterns, files: args.files }
|
||||
),
|
||||
brain_search: () => state.brainService!.searchMemories(
|
||||
args.query as string,
|
||||
args.limit as number | undefined,
|
||||
args.type as string | undefined
|
||||
),
|
||||
brain_relate: () => state.brainService!.relate(
|
||||
args.sourceConcept as string,
|
||||
args.targetConcept as string,
|
||||
args.relationType as string,
|
||||
args.weight as number | undefined
|
||||
),
|
||||
brain_context: () => state.brainService!.getContext(
|
||||
args.query as string,
|
||||
args.maxConcepts as number | undefined
|
||||
),
|
||||
brain_recall: () =>
|
||||
state.brainService!.recall(
|
||||
args.query as string,
|
||||
args.limit as number | undefined,
|
||||
),
|
||||
brain_learn: () =>
|
||||
state.brainService!.learn(
|
||||
args.name as string,
|
||||
args.whatItDoes as string,
|
||||
{ keywords: args.keywords, patterns: args.patterns, files: args.files },
|
||||
),
|
||||
brain_search: () =>
|
||||
state.brainService!.searchMemories(
|
||||
args.query as string,
|
||||
args.limit as number | undefined,
|
||||
args.type as string | undefined,
|
||||
),
|
||||
brain_relate: () =>
|
||||
state.brainService!.relate(
|
||||
args.sourceConcept as string,
|
||||
args.targetConcept as string,
|
||||
args.relationType as string,
|
||||
args.weight as number | undefined,
|
||||
),
|
||||
brain_context: () =>
|
||||
state.brainService!.getContext(
|
||||
args.query as string,
|
||||
args.maxConcepts as number | undefined,
|
||||
),
|
||||
brain_stats: () => state.brainService!.getStats(),
|
||||
brain_projects: async () => {
|
||||
// Import dynamically to avoid circular dependency
|
||||
@@ -172,7 +210,10 @@ const handleToolCall = async (
|
||||
|
||||
const handler = toolHandlers[toolName];
|
||||
if (!handler) {
|
||||
throw createMcpError(MCP_ERROR_CODES.TOOL_NOT_FOUND, `No handler for tool: ${toolName}`);
|
||||
throw createMcpError(
|
||||
MCP_ERROR_CODES.TOOL_NOT_FOUND,
|
||||
`No handler for tool: ${toolName}`,
|
||||
);
|
||||
}
|
||||
|
||||
result = await handler();
|
||||
@@ -180,19 +221,26 @@ const handleToolCall = async (
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
||||
text:
|
||||
typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const handleRequest = async (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse
|
||||
res: ServerResponse,
|
||||
): Promise<void> => {
|
||||
// Set CORS headers
|
||||
res.setHeader("Access-Control-Allow-Origin", state.config.allowedOrigins.join(","));
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Origin",
|
||||
state.config.allowedOrigins.join(","),
|
||||
);
|
||||
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", `Content-Type, ${state.config.apiKeyHeader}`);
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Headers",
|
||||
`Content-Type, ${state.config.apiKeyHeader}`,
|
||||
);
|
||||
|
||||
// Handle preflight
|
||||
if (req.method === "OPTIONS") {
|
||||
@@ -203,7 +251,11 @@ const handleRequest = async (
|
||||
|
||||
if (req.method !== "POST") {
|
||||
res.writeHead(405);
|
||||
res.end(JSON.stringify(createMcpResponse("", undefined, BRAIN_MCP_ERRORS.INVALID_REQUEST)));
|
||||
res.end(
|
||||
JSON.stringify(
|
||||
createMcpResponse("", undefined, BRAIN_MCP_ERRORS.INVALID_REQUEST),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,14 +265,22 @@ const handleRequest = async (
|
||||
// Check rate limit
|
||||
if (!checkRateLimit(clientIp)) {
|
||||
res.writeHead(429);
|
||||
res.end(JSON.stringify(createMcpResponse("", undefined, BRAIN_MCP_ERRORS.RATE_LIMITED)));
|
||||
res.end(
|
||||
JSON.stringify(
|
||||
createMcpResponse("", undefined, BRAIN_MCP_ERRORS.RATE_LIMITED),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate API key
|
||||
if (!validateApiKey(req)) {
|
||||
res.writeHead(401);
|
||||
res.end(JSON.stringify(createMcpResponse("", undefined, BRAIN_MCP_ERRORS.UNAUTHORIZED)));
|
||||
res.end(
|
||||
JSON.stringify(
|
||||
createMcpResponse("", undefined, BRAIN_MCP_ERRORS.UNAUTHORIZED),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -240,7 +300,11 @@ const handleRequest = async (
|
||||
mcpRequest = JSON.parse(body) as BrainMcpRequest;
|
||||
} catch {
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify(createMcpResponse("", undefined, BRAIN_MCP_ERRORS.PARSE_ERROR)));
|
||||
res.end(
|
||||
JSON.stringify(
|
||||
createMcpResponse("", undefined, BRAIN_MCP_ERRORS.PARSE_ERROR),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -258,21 +322,37 @@ const handleRequest = async (
|
||||
inputSchema: tool.inputSchema,
|
||||
}));
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
id: mcpRequest.id,
|
||||
result: { tools },
|
||||
}));
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
id: mcpRequest.id,
|
||||
result: { tools },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify(createMcpResponse(mcpRequest.id, undefined, BRAIN_MCP_ERRORS.METHOD_NOT_FOUND)));
|
||||
res.end(
|
||||
JSON.stringify(
|
||||
createMcpResponse(
|
||||
mcpRequest.id,
|
||||
undefined,
|
||||
BRAIN_MCP_ERRORS.METHOD_NOT_FOUND,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const mcpError = error instanceof Object && "code" in error
|
||||
? error as McpError
|
||||
: createMcpError(MCP_ERROR_CODES.INTERNAL_ERROR, error instanceof Error ? error.message : "Unknown error");
|
||||
const mcpError =
|
||||
error instanceof Object && "code" in error
|
||||
? (error as McpError)
|
||||
: createMcpError(
|
||||
MCP_ERROR_CODES.INTERNAL_ERROR,
|
||||
error instanceof Error ? error.message : "Unknown error",
|
||||
);
|
||||
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify(createMcpResponse(mcpRequest.id, undefined, mcpError)));
|
||||
res.end(
|
||||
JSON.stringify(createMcpResponse(mcpRequest.id, undefined, mcpError)),
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -281,7 +361,7 @@ const handleRequest = async (
|
||||
|
||||
export const start = async (
|
||||
brainService: BrainService,
|
||||
config?: Partial<BrainMcpServerConfig>
|
||||
config?: Partial<BrainMcpServerConfig>,
|
||||
): Promise<void> => {
|
||||
if (state.server) {
|
||||
throw new Error(BRAIN_MCP_MESSAGES.SERVER_ALREADY_RUNNING);
|
||||
@@ -348,5 +428,11 @@ export const updateConfig = (config: Partial<BrainMcpServerConfig>): void => {
|
||||
state.config = { ...state.config, ...config };
|
||||
};
|
||||
|
||||
export const getAvailableTools = (): ReadonlyArray<{ name: string; description: string }> =>
|
||||
BRAIN_MCP_TOOLS.map((t: BrainMcpTool) => ({ name: t.name, description: t.description }));
|
||||
export const getAvailableTools = (): ReadonlyArray<{
|
||||
name: string;
|
||||
description: string;
|
||||
}> =>
|
||||
BRAIN_MCP_TOOLS.map((t: BrainMcpTool) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
}));
|
||||
|
||||
@@ -128,7 +128,9 @@ export const enqueueBatch = async (items: SyncItem[]): Promise<number> => {
|
||||
/**
|
||||
* Get items from queue for processing
|
||||
*/
|
||||
export const dequeue = async (limit: number = SYNC_CONFIG.MAX_BATCH_SIZE): Promise<OfflineQueueItem[]> => {
|
||||
export const dequeue = async (
|
||||
limit: number = SYNC_CONFIG.MAX_BATCH_SIZE,
|
||||
): Promise<OfflineQueueItem[]> => {
|
||||
await loadQueue();
|
||||
|
||||
// Get items that haven't exceeded retry limit
|
||||
|
||||
@@ -39,7 +39,13 @@ interface ProjectServiceState {
|
||||
const state: ProjectServiceState = {
|
||||
projects: new Map(),
|
||||
activeProjectId: null,
|
||||
configPath: join(homedir(), ".local", "share", "codetyper", BRAIN_PROJECT_STORAGE.CONFIG_FILE),
|
||||
configPath: join(
|
||||
homedir(),
|
||||
".local",
|
||||
"share",
|
||||
"codetyper",
|
||||
BRAIN_PROJECT_STORAGE.CONFIG_FILE,
|
||||
),
|
||||
initialized: false,
|
||||
};
|
||||
|
||||
@@ -116,7 +122,9 @@ export const initialize = async (): Promise<void> => {
|
||||
state.initialized = true;
|
||||
};
|
||||
|
||||
export const createProject = async (input: BrainProjectCreateInput): Promise<BrainProject> => {
|
||||
export const createProject = async (
|
||||
input: BrainProjectCreateInput,
|
||||
): Promise<BrainProject> => {
|
||||
await initialize();
|
||||
|
||||
// Validate name
|
||||
@@ -130,7 +138,7 @@ export const createProject = async (input: BrainProjectCreateInput): Promise<Bra
|
||||
|
||||
// Check for duplicate names
|
||||
const existingProject = Array.from(state.projects.values()).find(
|
||||
(p) => p.name.toLowerCase() === input.name.toLowerCase()
|
||||
(p) => p.name.toLowerCase() === input.name.toLowerCase(),
|
||||
);
|
||||
|
||||
if (existingProject) {
|
||||
@@ -161,7 +169,7 @@ export const createProject = async (input: BrainProjectCreateInput): Promise<Bra
|
||||
|
||||
export const updateProject = async (
|
||||
projectId: number,
|
||||
input: BrainProjectUpdateInput
|
||||
input: BrainProjectUpdateInput,
|
||||
): Promise<BrainProject> => {
|
||||
await initialize();
|
||||
|
||||
@@ -205,7 +213,9 @@ export const deleteProject = async (projectId: number): Promise<boolean> => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const switchProject = async (projectId: number): Promise<BrainProjectSwitchResult> => {
|
||||
export const switchProject = async (
|
||||
projectId: number,
|
||||
): Promise<BrainProjectSwitchResult> => {
|
||||
await initialize();
|
||||
|
||||
const newProject = state.projects.get(projectId);
|
||||
@@ -219,7 +229,10 @@ export const switchProject = async (projectId: number): Promise<BrainProjectSwit
|
||||
|
||||
// Update active status
|
||||
if (previousProject) {
|
||||
state.projects.set(previousProject.id, { ...previousProject, isActive: false });
|
||||
state.projects.set(previousProject.id, {
|
||||
...previousProject,
|
||||
isActive: false,
|
||||
});
|
||||
}
|
||||
|
||||
state.projects.set(projectId, { ...newProject, isActive: true });
|
||||
@@ -235,35 +248,45 @@ export const switchProject = async (projectId: number): Promise<BrainProjectSwit
|
||||
};
|
||||
};
|
||||
|
||||
export const getProject = async (projectId: number): Promise<BrainProject | undefined> => {
|
||||
export const getProject = async (
|
||||
projectId: number,
|
||||
): Promise<BrainProject | undefined> => {
|
||||
await initialize();
|
||||
return state.projects.get(projectId);
|
||||
};
|
||||
|
||||
export const getActiveProject = async (): Promise<BrainProject | undefined> => {
|
||||
await initialize();
|
||||
return state.activeProjectId ? state.projects.get(state.activeProjectId) : undefined;
|
||||
return state.activeProjectId
|
||||
? state.projects.get(state.activeProjectId)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export const listProjects = async (): Promise<BrainProjectListResult> => {
|
||||
await initialize();
|
||||
|
||||
return {
|
||||
projects: Array.from(state.projects.values()).sort((a, b) => b.updatedAt - a.updatedAt),
|
||||
projects: Array.from(state.projects.values()).sort(
|
||||
(a, b) => b.updatedAt - a.updatedAt,
|
||||
),
|
||||
activeProjectId: state.activeProjectId ?? undefined,
|
||||
total: state.projects.size,
|
||||
};
|
||||
};
|
||||
|
||||
export const findProjectByPath = async (rootPath: string): Promise<BrainProject | undefined> => {
|
||||
export const findProjectByPath = async (
|
||||
rootPath: string,
|
||||
): Promise<BrainProject | undefined> => {
|
||||
await initialize();
|
||||
|
||||
return Array.from(state.projects.values()).find((p) => p.rootPath === rootPath);
|
||||
return Array.from(state.projects.values()).find(
|
||||
(p) => p.rootPath === rootPath,
|
||||
);
|
||||
};
|
||||
|
||||
export const updateProjectStats = async (
|
||||
projectId: number,
|
||||
stats: Partial<BrainProjectStats>
|
||||
stats: Partial<BrainProjectStats>,
|
||||
): Promise<void> => {
|
||||
await initialize();
|
||||
|
||||
@@ -280,7 +303,9 @@ export const updateProjectStats = async (
|
||||
await saveProjectsToConfig();
|
||||
};
|
||||
|
||||
export const exportProject = async (projectId: number): Promise<BrainProjectExport> => {
|
||||
export const exportProject = async (
|
||||
projectId: number,
|
||||
): Promise<BrainProjectExport> => {
|
||||
await initialize();
|
||||
|
||||
const project = state.projects.get(projectId);
|
||||
@@ -307,7 +332,7 @@ export const exportProject = async (projectId: number): Promise<BrainProjectExpo
|
||||
"codetyper",
|
||||
"brain",
|
||||
"exports",
|
||||
`${project.name}-${Date.now()}${BRAIN_PROJECT_STORAGE.EXPORT_EXTENSION}`
|
||||
`${project.name}-${Date.now()}${BRAIN_PROJECT_STORAGE.EXPORT_EXTENSION}`,
|
||||
);
|
||||
|
||||
await writeFile(exportPath, JSON.stringify(exportData, null, 2));
|
||||
@@ -316,7 +341,7 @@ export const exportProject = async (projectId: number): Promise<BrainProjectExpo
|
||||
};
|
||||
|
||||
export const importProject = async (
|
||||
exportData: BrainProjectExport
|
||||
exportData: BrainProjectExport,
|
||||
): Promise<BrainProjectImportResult> => {
|
||||
await initialize();
|
||||
|
||||
@@ -352,7 +377,9 @@ export const importProject = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const getProjectSettings = async (projectId: number): Promise<BrainProjectSettings | undefined> => {
|
||||
export const getProjectSettings = async (
|
||||
projectId: number,
|
||||
): Promise<BrainProjectSettings | undefined> => {
|
||||
await initialize();
|
||||
|
||||
const project = state.projects.get(projectId);
|
||||
@@ -361,13 +388,15 @@ export const getProjectSettings = async (projectId: number): Promise<BrainProjec
|
||||
|
||||
export const updateProjectSettings = async (
|
||||
projectId: number,
|
||||
settings: Partial<BrainProjectSettings>
|
||||
settings: Partial<BrainProjectSettings>,
|
||||
): Promise<BrainProjectSettings> => {
|
||||
const project = await updateProject(projectId, { settings });
|
||||
return project.settings;
|
||||
};
|
||||
|
||||
export const setActiveProjectByPath = async (rootPath: string): Promise<BrainProject | undefined> => {
|
||||
export const setActiveProjectByPath = async (
|
||||
rootPath: string,
|
||||
): Promise<BrainProject | undefined> => {
|
||||
const project = await findProjectByPath(rootPath);
|
||||
|
||||
if (project) {
|
||||
|
||||
@@ -12,13 +12,13 @@ import type {
|
||||
} from "@/types/provider-quality";
|
||||
import { PROVIDER_IDS } from "@constants/provider-quality";
|
||||
import { parseAuditResponse } from "@prompts/audit-prompt";
|
||||
import { detectTaskType } from "@services/provider-quality/task-detector";
|
||||
import { determineRoute } from "@services/provider-quality/router";
|
||||
import {
|
||||
detectTaskType,
|
||||
determineRoute,
|
||||
recordAuditResult,
|
||||
recordApproval,
|
||||
recordRejection,
|
||||
} from "@services/provider-quality";
|
||||
} from "@services/provider-quality/score-manager";
|
||||
import {
|
||||
checkOllamaAvailability,
|
||||
checkCopilotAvailability,
|
||||
@@ -39,7 +39,11 @@ export interface CascadeOptions {
|
||||
}
|
||||
|
||||
export interface ProviderCallFn {
|
||||
(prompt: string, provider: "ollama" | "copilot", isAudit?: boolean): Promise<string>;
|
||||
(
|
||||
prompt: string,
|
||||
provider: "ollama" | "copilot",
|
||||
isAudit?: boolean,
|
||||
): Promise<string>;
|
||||
}
|
||||
|
||||
export const executeCascade = async (
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
*/
|
||||
|
||||
import { AUTH_MESSAGES } from "@constants/chat-service";
|
||||
import { getProviderStatus } from "@providers/core/status";
|
||||
import { getCopilotUserInfo } from "@providers/copilot/user-info";
|
||||
import { logoutProvider } from "@providers/login/handlers";
|
||||
import {
|
||||
getProviderStatus,
|
||||
getCopilotUserInfo,
|
||||
logoutProvider,
|
||||
initiateDeviceFlow,
|
||||
pollForAccessToken,
|
||||
completeCopilotLogin,
|
||||
} from "@providers/index";
|
||||
import { appStore } from "@tui/index";
|
||||
} from "@providers/copilot/auth/auth";
|
||||
import { completeCopilotLogin } from "@providers/login/core/initialize";
|
||||
import { appStore } from "@tui-solid/context/app";
|
||||
import { loadModels } from "@services/chat-tui/models";
|
||||
import type {
|
||||
ChatServiceState,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { saveSession as saveSessionSession } from "@services/core/session";
|
||||
import { appStore } from "@tui/index";
|
||||
import { appStore } from "@tui-solid/context/app";
|
||||
import { CHAT_MESSAGES, type CommandName } from "@constants/chat-service";
|
||||
import { handleLogin, handleLogout, showWhoami } from "@services/chat-tui/auth";
|
||||
import {
|
||||
@@ -14,8 +14,8 @@ import { showUsageStats } from "@services/chat-tui/usage";
|
||||
import {
|
||||
checkOllamaAvailability,
|
||||
checkCopilotAvailability,
|
||||
} from "@services/cascading-provider";
|
||||
import { getOverallScore } from "@services/provider-quality";
|
||||
} from "@services/cascading-provider/availability";
|
||||
import { getOverallScore } from "@services/provider-quality/score-manager";
|
||||
import { PROVIDER_IDS } from "@constants/provider-quality";
|
||||
import type {
|
||||
ChatServiceState,
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
BINARY_EXTENSIONS,
|
||||
type BinaryExtension,
|
||||
} from "@constants/file-picker";
|
||||
import { appStore } from "@tui/index";
|
||||
import { appStore } from "@tui-solid/context/app";
|
||||
import type { ChatServiceState } from "@/types/chat-service";
|
||||
|
||||
const isBinaryFile = (filePath: string): boolean => {
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
} from "@services/core/session";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import { initializePermissions } from "@services/core/permissions";
|
||||
import { getProviderStatus } from "@providers/index";
|
||||
import { appStore } from "@tui/index";
|
||||
import { getProviderStatus } from "@providers/core/status";
|
||||
import { appStore } from "@tui-solid/context/app";
|
||||
import { themeActions } from "@stores/core/theme-store";
|
||||
import {
|
||||
buildBaseContext,
|
||||
@@ -23,7 +23,7 @@ import * as brainService from "@services/brain";
|
||||
import { BRAIN_DISABLED } from "@constants/brain";
|
||||
import { addContextFile } from "@services/chat-tui/files";
|
||||
import type { ProviderName, Message } from "@/types/providers";
|
||||
import type { ChatSession } from "@/types/index";
|
||||
import type { ChatSession } from "@/types/common";
|
||||
import type { ChatTUIOptions } from "@interfaces/ChatTUIOptions";
|
||||
import type { ChatServiceState } from "@/types/chat-service";
|
||||
import type { InteractionMode } from "@/types/tui";
|
||||
|
||||
@@ -6,14 +6,13 @@ import { addMessage, saveSession } from "@services/core/session";
|
||||
import { createStreamingAgent } from "@services/agent-stream";
|
||||
import { CHAT_MESSAGES } from "@constants/chat-service";
|
||||
import { enrichMessageWithIssues } from "@services/github-issue-service";
|
||||
import { checkGitHubCLI } from "@services/github-pr/cli";
|
||||
import { extractPRUrls } from "@services/github-pr/url";
|
||||
import { fetchPR, fetchPRComments } from "@services/github-pr/fetch";
|
||||
import {
|
||||
checkGitHubCLI,
|
||||
extractPRUrls,
|
||||
fetchPR,
|
||||
fetchPRComments,
|
||||
formatPRContext,
|
||||
formatPendingComments,
|
||||
} from "@services/github-pr";
|
||||
} from "@services/github-pr/format";
|
||||
import {
|
||||
analyzeFileChange,
|
||||
clearSuggestions,
|
||||
@@ -33,21 +32,25 @@ import {
|
||||
getModelCompactionConfig,
|
||||
checkCompactionNeeded,
|
||||
} from "@services/auto-compaction";
|
||||
import { detectTaskType } from "@services/provider-quality/task-detector";
|
||||
import {
|
||||
detectTaskType,
|
||||
determineRoute,
|
||||
recordAuditResult,
|
||||
isCorrection,
|
||||
getRoutingExplanation,
|
||||
} from "@services/provider-quality";
|
||||
} from "@services/provider-quality/router";
|
||||
import { recordAuditResult } from "@services/provider-quality/score-manager";
|
||||
import { isCorrection } from "@services/provider-quality/feedback-detector";
|
||||
import {
|
||||
checkOllamaAvailability,
|
||||
checkCopilotAvailability,
|
||||
} from "@services/cascading-provider";
|
||||
} from "@services/cascading-provider/availability";
|
||||
import { chat, getDefaultModel } from "@providers/core/chat";
|
||||
import { AUDIT_SYSTEM_PROMPT, createAuditPrompt, parseAuditResponse } from "@prompts/audit-prompt";
|
||||
import {
|
||||
AUDIT_SYSTEM_PROMPT,
|
||||
createAuditPrompt,
|
||||
parseAuditResponse,
|
||||
} from "@prompts/audit-prompt";
|
||||
import { PROVIDER_IDS } from "@constants/provider-quality";
|
||||
import { appStore } from "@tui/index";
|
||||
import { appStore } from "@tui-solid/context/app";
|
||||
import type { StreamCallbacks } from "@/types/streaming";
|
||||
import type { TaskType } from "@/types/provider-quality";
|
||||
import type {
|
||||
@@ -62,10 +65,7 @@ import {
|
||||
detectCommand,
|
||||
executeDetectedCommand,
|
||||
} from "@services/command-detection";
|
||||
import {
|
||||
detectSkillCommand,
|
||||
executeSkill,
|
||||
} from "@services/skill-service";
|
||||
import { detectSkillCommand, executeSkill } from "@services/skill-service";
|
||||
|
||||
// Track last response for feedback learning
|
||||
let lastResponseContext: {
|
||||
@@ -101,7 +101,10 @@ const createToolCallHandler =
|
||||
) =>
|
||||
(call: { id: string; name: string; arguments?: Record<string, unknown> }) => {
|
||||
const args = call.arguments;
|
||||
if ((FILE_MODIFYING_TOOLS as readonly string[]).includes(call.name) && args?.path) {
|
||||
if (
|
||||
(FILE_MODIFYING_TOOLS as readonly string[]).includes(call.name) &&
|
||||
args?.path
|
||||
) {
|
||||
toolCallRef.current = { name: call.name, path: String(args.path) };
|
||||
} else {
|
||||
toolCallRef.current = { name: call.name };
|
||||
@@ -152,7 +155,10 @@ const createStreamCallbacks = (): StreamCallbacksWithState => {
|
||||
const callbacks: StreamCallbacks = {
|
||||
onContentChunk: (content: string) => {
|
||||
chunkCount++;
|
||||
addDebugLog("stream", `Chunk #${chunkCount}: "${content.substring(0, 30)}${content.length > 30 ? "..." : ""}"`);
|
||||
addDebugLog(
|
||||
"stream",
|
||||
`Chunk #${chunkCount}: "${content.substring(0, 30)}${content.length > 30 ? "..." : ""}"`,
|
||||
);
|
||||
appStore.appendStreamContent(content);
|
||||
},
|
||||
|
||||
@@ -187,7 +193,10 @@ const createStreamCallbacks = (): StreamCallbacksWithState => {
|
||||
// Note: Don't call completeStreaming() here!
|
||||
// The agent loop may have multiple iterations (tool calls + final response)
|
||||
// Streaming will be completed manually after the entire agent finishes
|
||||
addDebugLog("stream", `Stream iteration done (${chunkCount} chunks total)`);
|
||||
addDebugLog(
|
||||
"stream",
|
||||
`Stream iteration done (${chunkCount} chunks total)`,
|
||||
);
|
||||
},
|
||||
|
||||
onError: (error: string) => {
|
||||
@@ -213,13 +222,20 @@ const runAudit = async (
|
||||
userPrompt: string,
|
||||
ollamaResponse: string,
|
||||
callbacks: ChatServiceCallbacks,
|
||||
): Promise<{ approved: boolean; hasMajorIssues: boolean; correctedResponse?: string }> => {
|
||||
): Promise<{
|
||||
approved: boolean;
|
||||
hasMajorIssues: boolean;
|
||||
correctedResponse?: string;
|
||||
}> => {
|
||||
try {
|
||||
callbacks.onLog("system", "Auditing response with Copilot...");
|
||||
|
||||
const auditMessages = [
|
||||
{ role: "system" as const, content: AUDIT_SYSTEM_PROMPT },
|
||||
{ role: "user" as const, content: createAuditPrompt(userPrompt, ollamaResponse) },
|
||||
{
|
||||
role: "user" as const,
|
||||
content: createAuditPrompt(userPrompt, ollamaResponse),
|
||||
},
|
||||
];
|
||||
|
||||
const auditResponse = await chat("copilot", auditMessages, {});
|
||||
@@ -237,7 +253,8 @@ const runAudit = async (
|
||||
|
||||
return {
|
||||
approved: parsed.approved,
|
||||
hasMajorIssues: parsed.severity === "major" || parsed.severity === "critical",
|
||||
hasMajorIssues:
|
||||
parsed.severity === "major" || parsed.severity === "critical",
|
||||
correctedResponse: parsed.correctedResponse,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -285,10 +302,7 @@ export const handleMessage = async (
|
||||
const skillMatch = await detectSkillCommand(message);
|
||||
if (skillMatch) {
|
||||
addDebugLog("info", `Detected skill: /${skillMatch.skill.command}`);
|
||||
callbacks.onLog(
|
||||
"system",
|
||||
`Running skill: ${skillMatch.skill.name}`,
|
||||
);
|
||||
callbacks.onLog("system", `Running skill: ${skillMatch.skill.name}`);
|
||||
|
||||
// Execute the skill and get the expanded prompt
|
||||
const { expandedPrompt } = executeSkill(skillMatch.skill, skillMatch.args);
|
||||
@@ -302,7 +316,10 @@ export const handleMessage = async (
|
||||
// Process the expanded prompt as the actual message
|
||||
// Fall through to normal processing with the expanded prompt
|
||||
message = expandedPrompt;
|
||||
addDebugLog("info", `Expanded skill prompt: ${expandedPrompt.substring(0, 100)}...`);
|
||||
addDebugLog(
|
||||
"info",
|
||||
`Expanded skill prompt: ${expandedPrompt.substring(0, 100)}...`,
|
||||
);
|
||||
}
|
||||
|
||||
// Detect explicit command requests and execute directly
|
||||
@@ -328,7 +345,10 @@ export const handleMessage = async (
|
||||
});
|
||||
|
||||
appStore.setMode("tool_execution");
|
||||
const result = await executeDetectedCommand(detected.command, process.cwd());
|
||||
const result = await executeDetectedCommand(
|
||||
detected.command,
|
||||
process.cwd(),
|
||||
);
|
||||
appStore.setMode("idle");
|
||||
|
||||
// Show result
|
||||
@@ -351,15 +371,13 @@ export const handleMessage = async (
|
||||
|
||||
// Get interaction mode and cascade setting from app store
|
||||
const { interactionMode, cascadeEnabled } = appStore.getState();
|
||||
const isReadOnlyMode = interactionMode === "ask" || interactionMode === "code-review";
|
||||
const isReadOnlyMode =
|
||||
interactionMode === "ask" || interactionMode === "code-review";
|
||||
|
||||
// Rebuild system prompt if mode has changed
|
||||
if (state.currentMode !== interactionMode) {
|
||||
await rebuildSystemPromptForMode(state, interactionMode);
|
||||
callbacks.onLog(
|
||||
"system",
|
||||
`Switched to ${interactionMode} mode`,
|
||||
);
|
||||
callbacks.onLog("system", `Switched to ${interactionMode} mode`);
|
||||
}
|
||||
|
||||
if (isReadOnlyMode) {
|
||||
@@ -494,7 +512,10 @@ export const handleMessage = async (
|
||||
cascadeEnabled: true,
|
||||
});
|
||||
|
||||
const explanation = await getRoutingExplanation(routingDecision, taskType);
|
||||
const explanation = await getRoutingExplanation(
|
||||
routingDecision,
|
||||
taskType,
|
||||
);
|
||||
callbacks.onLog("system", explanation);
|
||||
|
||||
if (routingDecision === "ollama_only") {
|
||||
@@ -518,8 +539,14 @@ export const handleMessage = async (
|
||||
: getDefaultModel(effectiveProvider);
|
||||
|
||||
// Start streaming UI
|
||||
addDebugLog("state", `Starting request: provider=${effectiveProvider}, model=${effectiveModel}`);
|
||||
addDebugLog("state", `Mode: ${appStore.getState().interactionMode}, Cascade: ${cascadeEnabled}`);
|
||||
addDebugLog(
|
||||
"state",
|
||||
`Starting request: provider=${effectiveProvider}, model=${effectiveModel}`,
|
||||
);
|
||||
addDebugLog(
|
||||
"state",
|
||||
`Mode: ${appStore.getState().interactionMode}, Cascade: ${cascadeEnabled}`,
|
||||
);
|
||||
appStore.setMode("thinking");
|
||||
appStore.startThinking();
|
||||
appStore.startStreaming();
|
||||
@@ -554,20 +581,33 @@ export const handleMessage = async (
|
||||
currentAgent = agent;
|
||||
|
||||
try {
|
||||
addDebugLog("api", `Agent.run() started with ${state.messages.length} messages`);
|
||||
addDebugLog(
|
||||
"api",
|
||||
`Agent.run() started with ${state.messages.length} messages`,
|
||||
);
|
||||
const result = await agent.run(state.messages);
|
||||
addDebugLog("api", `Agent.run() completed: success=${result.success}, iterations=${result.iterations}`);
|
||||
addDebugLog(
|
||||
"api",
|
||||
`Agent.run() completed: success=${result.success}, iterations=${result.iterations}`,
|
||||
);
|
||||
|
||||
// Stop thinking timer
|
||||
appStore.stopThinking();
|
||||
|
||||
if (result.finalResponse) {
|
||||
addDebugLog("info", `Final response length: ${result.finalResponse.length} chars`);
|
||||
addDebugLog(
|
||||
"info",
|
||||
`Final response length: ${result.finalResponse.length} chars`,
|
||||
);
|
||||
let finalResponse = result.finalResponse;
|
||||
|
||||
// Run audit if cascade mode with Ollama
|
||||
if (shouldAudit && effectiveProvider === "ollama") {
|
||||
const auditResult = await runAudit(message, result.finalResponse, callbacks);
|
||||
const auditResult = await runAudit(
|
||||
message,
|
||||
result.finalResponse,
|
||||
callbacks,
|
||||
);
|
||||
|
||||
// Record quality score based on audit
|
||||
await recordAuditResult(
|
||||
@@ -599,7 +639,10 @@ export const handleMessage = async (
|
||||
// Check if streaming content was received - if not, add the response as a log
|
||||
// This handles cases where streaming didn't work or content was all in final response
|
||||
if (!streamState.hasReceivedContent() && finalResponse) {
|
||||
addDebugLog("info", "No streaming content received, adding fallback log");
|
||||
addDebugLog(
|
||||
"info",
|
||||
"No streaming content received, adding fallback log",
|
||||
);
|
||||
// Streaming didn't receive content, manually add the response
|
||||
appStore.cancelStreaming(); // Remove empty streaming log
|
||||
appStore.addLog({
|
||||
@@ -616,11 +659,7 @@ export const handleMessage = async (
|
||||
addMessage("assistant", finalResponse);
|
||||
await saveSession();
|
||||
|
||||
await processLearningsFromExchange(
|
||||
message,
|
||||
finalResponse,
|
||||
callbacks,
|
||||
);
|
||||
await processLearningsFromExchange(message, finalResponse, callbacks);
|
||||
|
||||
const suggestions = getPendingSuggestions();
|
||||
if (suggestions.length > 0) {
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
import { MODEL_MESSAGES } from "@constants/chat-service";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import { getProvider } from "@providers/core/registry";
|
||||
import {
|
||||
getProvider,
|
||||
getDefaultModel,
|
||||
getModels as getProviderModels,
|
||||
} from "@providers/index";
|
||||
import { appStore } from "@tui/index";
|
||||
} from "@providers/core/chat";
|
||||
import { appStore } from "@tui-solid/context/app";
|
||||
import type { ProviderName, ProviderModel } from "@/types/providers";
|
||||
import type {
|
||||
ChatServiceState,
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
PermissionPromptRequest,
|
||||
PermissionPromptResponse,
|
||||
} from "@/types/permissions";
|
||||
import { appStore } from "@tui/index";
|
||||
import { appStore } from "@tui-solid/context/app";
|
||||
|
||||
export const createPermissionHandler = (): ((
|
||||
request: PermissionPromptRequest,
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
} from "@/types/streaming";
|
||||
import type { ToolCall, ToolResult } from "@/types/tools";
|
||||
import { createStreamingAgent } from "@services/agent-stream";
|
||||
import { appStore } from "@tui/index";
|
||||
import { appStore } from "@tui-solid/context/app";
|
||||
|
||||
// Re-export for convenience
|
||||
export type { StreamingChatOptions } from "@interfaces/StreamingChatOptions";
|
||||
|
||||
@@ -55,7 +55,9 @@ const formatQuotaBar = (
|
||||
|
||||
if (quota.unlimited) {
|
||||
lines.push(name);
|
||||
lines.push(PROGRESS_BAR.FILLED_CHAR.repeat(PROGRESS_BAR.WIDTH) + " Unlimited");
|
||||
lines.push(
|
||||
PROGRESS_BAR.FILLED_CHAR.repeat(PROGRESS_BAR.WIDTH) + " Unlimited",
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,12 @@ const runCommand = (
|
||||
|
||||
const detectImageType = (buffer: Buffer): ImageMediaType | null => {
|
||||
// PNG: 89 50 4E 47
|
||||
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
|
||||
if (
|
||||
buffer[0] === 0x89 &&
|
||||
buffer[1] === 0x50 &&
|
||||
buffer[2] === 0x4e &&
|
||||
buffer[3] === 0x47
|
||||
) {
|
||||
return "image/png";
|
||||
}
|
||||
// JPEG: FF D8 FF
|
||||
@@ -66,12 +71,27 @@ const detectImageType = (buffer: Buffer): ImageMediaType | null => {
|
||||
return "image/jpeg";
|
||||
}
|
||||
// GIF: 47 49 46 38
|
||||
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) {
|
||||
if (
|
||||
buffer[0] === 0x47 &&
|
||||
buffer[1] === 0x49 &&
|
||||
buffer[2] === 0x46 &&
|
||||
buffer[3] === 0x38
|
||||
) {
|
||||
return "image/gif";
|
||||
}
|
||||
// WebP: 52 49 46 46 ... 57 45 42 50
|
||||
if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46) {
|
||||
if (buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50) {
|
||||
if (
|
||||
buffer[0] === 0x52 &&
|
||||
buffer[1] === 0x49 &&
|
||||
buffer[2] === 0x46 &&
|
||||
buffer[3] === 0x46
|
||||
) {
|
||||
if (
|
||||
buffer[8] === 0x57 &&
|
||||
buffer[9] === 0x45 &&
|
||||
buffer[10] === 0x42 &&
|
||||
buffer[11] === 0x50
|
||||
) {
|
||||
return "image/webp";
|
||||
}
|
||||
}
|
||||
@@ -134,7 +154,10 @@ const readClipboardImageMacOS = async (): Promise<PastedImage | null> => {
|
||||
const readClipboardImageLinux = async (): Promise<PastedImage | null> => {
|
||||
// Try xclip first, then wl-paste for Wayland
|
||||
const commands = [
|
||||
{ cmd: "xclip", args: ["-selection", "clipboard", "-t", "image/png", "-o"] },
|
||||
{
|
||||
cmd: "xclip",
|
||||
args: ["-selection", "clipboard", "-t", "image/png", "-o"],
|
||||
},
|
||||
{ cmd: "wl-paste", args: ["--type", "image/png"] },
|
||||
];
|
||||
|
||||
|
||||
@@ -16,15 +16,24 @@ import {
|
||||
CONFIDENCE_LEVELS,
|
||||
DEFAULT_CONFIDENCE_FILTER_CONFIG,
|
||||
} from "@/types/confidence-filter";
|
||||
import { CONFIDENCE_FILTER, CONFIDENCE_WEIGHTS } from "@constants/confidence-filter";
|
||||
import {
|
||||
CONFIDENCE_FILTER,
|
||||
CONFIDENCE_WEIGHTS,
|
||||
} from "@constants/confidence-filter";
|
||||
|
||||
export const calculateConfidenceLevel = (score: number): ConfidenceLevel => {
|
||||
const levels = Object.entries(CONFIDENCE_LEVELS) as Array<[ConfidenceLevel, { min: number; max: number }]>;
|
||||
const found = levels.find(([, range]) => score >= range.min && score <= range.max);
|
||||
const levels = Object.entries(CONFIDENCE_LEVELS) as Array<
|
||||
[ConfidenceLevel, { min: number; max: number }]
|
||||
>;
|
||||
const found = levels.find(
|
||||
([, range]) => score >= range.min && score <= range.max,
|
||||
);
|
||||
return found ? found[0] : "low";
|
||||
};
|
||||
|
||||
export const calculateConfidenceScore = (factors: ReadonlyArray<ConfidenceFactor>): ConfidenceScore => {
|
||||
export const calculateConfidenceScore = (
|
||||
factors: ReadonlyArray<ConfidenceFactor>,
|
||||
): ConfidenceScore => {
|
||||
const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
|
||||
const weightedSum = factors.reduce((sum, f) => sum + f.score * f.weight, 0);
|
||||
const value = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0;
|
||||
@@ -40,7 +49,7 @@ export const createConfidenceFactor = (
|
||||
name: string,
|
||||
score: number,
|
||||
weight: number,
|
||||
reason: string
|
||||
reason: string,
|
||||
): ConfidenceFactor => ({
|
||||
name,
|
||||
score: Math.max(0, Math.min(100, score)),
|
||||
@@ -48,51 +57,67 @@ export const createConfidenceFactor = (
|
||||
reason,
|
||||
});
|
||||
|
||||
export const createPatternMatchFactor = (matchCount: number, expectedCount: number): ConfidenceFactor =>
|
||||
export const createPatternMatchFactor = (
|
||||
matchCount: number,
|
||||
expectedCount: number,
|
||||
): ConfidenceFactor =>
|
||||
createConfidenceFactor(
|
||||
"Pattern Match",
|
||||
Math.min(100, (matchCount / Math.max(1, expectedCount)) * 100),
|
||||
CONFIDENCE_WEIGHTS.PATTERN_MATCH,
|
||||
`Matched ${matchCount}/${expectedCount} expected patterns`
|
||||
`Matched ${matchCount}/${expectedCount} expected patterns`,
|
||||
);
|
||||
|
||||
export const createContextRelevanceFactor = (relevanceScore: number): ConfidenceFactor =>
|
||||
export const createContextRelevanceFactor = (
|
||||
relevanceScore: number,
|
||||
): ConfidenceFactor =>
|
||||
createConfidenceFactor(
|
||||
"Context Relevance",
|
||||
relevanceScore,
|
||||
CONFIDENCE_WEIGHTS.CONTEXT_RELEVANCE,
|
||||
`Context relevance score: ${relevanceScore}%`
|
||||
`Context relevance score: ${relevanceScore}%`,
|
||||
);
|
||||
|
||||
export const createSeverityFactor = (severity: "low" | "medium" | "high" | "critical"): ConfidenceFactor => {
|
||||
const severityScores: Record<string, number> = { low: 40, medium: 60, high: 80, critical: 95 };
|
||||
export const createSeverityFactor = (
|
||||
severity: "low" | "medium" | "high" | "critical",
|
||||
): ConfidenceFactor => {
|
||||
const severityScores: Record<string, number> = {
|
||||
low: 40,
|
||||
medium: 60,
|
||||
high: 80,
|
||||
critical: 95,
|
||||
};
|
||||
return createConfidenceFactor(
|
||||
"Severity Level",
|
||||
severityScores[severity] ?? 50,
|
||||
CONFIDENCE_WEIGHTS.SEVERITY_LEVEL,
|
||||
`Issue severity: ${severity}`
|
||||
`Issue severity: ${severity}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const createCodeAnalysisFactor = (analysisScore: number): ConfidenceFactor =>
|
||||
export const createCodeAnalysisFactor = (
|
||||
analysisScore: number,
|
||||
): ConfidenceFactor =>
|
||||
createConfidenceFactor(
|
||||
"Code Analysis",
|
||||
analysisScore,
|
||||
CONFIDENCE_WEIGHTS.CODE_ANALYSIS,
|
||||
`Static analysis confidence: ${analysisScore}%`
|
||||
`Static analysis confidence: ${analysisScore}%`,
|
||||
);
|
||||
|
||||
export const createHistoricalAccuracyFactor = (accuracy: number): ConfidenceFactor =>
|
||||
export const createHistoricalAccuracyFactor = (
|
||||
accuracy: number,
|
||||
): ConfidenceFactor =>
|
||||
createConfidenceFactor(
|
||||
"Historical Accuracy",
|
||||
accuracy,
|
||||
CONFIDENCE_WEIGHTS.HISTORICAL_ACCURACY,
|
||||
`Historical accuracy for similar issues: ${accuracy}%`
|
||||
`Historical accuracy for similar issues: ${accuracy}%`,
|
||||
);
|
||||
|
||||
export const filterByConfidence = <T>(
|
||||
items: ReadonlyArray<{ item: T; confidence: ConfidenceScore }>,
|
||||
config: ConfidenceFilterConfig = DEFAULT_CONFIDENCE_FILTER_CONFIG
|
||||
config: ConfidenceFilterConfig = DEFAULT_CONFIDENCE_FILTER_CONFIG,
|
||||
): ReadonlyArray<FilteredResult<T>> =>
|
||||
items.map(({ item, confidence }) => ({
|
||||
item,
|
||||
@@ -100,11 +125,12 @@ export const filterByConfidence = <T>(
|
||||
passed: confidence.value >= config.minThreshold,
|
||||
}));
|
||||
|
||||
export const filterPassedOnly = <T>(results: ReadonlyArray<FilteredResult<T>>): ReadonlyArray<T> =>
|
||||
results.filter((r) => r.passed).map((r) => r.item);
|
||||
export const filterPassedOnly = <T>(
|
||||
results: ReadonlyArray<FilteredResult<T>>,
|
||||
): ReadonlyArray<T> => results.filter((r) => r.passed).map((r) => r.item);
|
||||
|
||||
export const groupByConfidenceLevel = <T>(
|
||||
results: ReadonlyArray<FilteredResult<T>>
|
||||
results: ReadonlyArray<FilteredResult<T>>,
|
||||
): Record<ConfidenceLevel, ReadonlyArray<FilteredResult<T>>> => ({
|
||||
low: results.filter((r) => r.confidence.level === "low"),
|
||||
medium: results.filter((r) => r.confidence.level === "medium"),
|
||||
@@ -112,10 +138,15 @@ export const groupByConfidenceLevel = <T>(
|
||||
critical: results.filter((r) => r.confidence.level === "critical"),
|
||||
});
|
||||
|
||||
export const calculateFilterStats = <T>(results: ReadonlyArray<FilteredResult<T>>): ConfidenceFilterStats => {
|
||||
export const calculateFilterStats = <T>(
|
||||
results: ReadonlyArray<FilteredResult<T>>,
|
||||
): ConfidenceFilterStats => {
|
||||
const passed = results.filter((r) => r.passed).length;
|
||||
const grouped = groupByConfidenceLevel(results);
|
||||
const totalConfidence = results.reduce((sum, r) => sum + r.confidence.value, 0);
|
||||
const totalConfidence = results.reduce(
|
||||
(sum, r) => sum + r.confidence.value,
|
||||
0,
|
||||
);
|
||||
|
||||
return {
|
||||
total: results.length,
|
||||
@@ -127,24 +158,33 @@ export const calculateFilterStats = <T>(results: ReadonlyArray<FilteredResult<T>
|
||||
high: grouped.high.length,
|
||||
critical: grouped.critical.length,
|
||||
},
|
||||
averageConfidence: results.length > 0 ? Math.round(totalConfidence / results.length) : 0,
|
||||
averageConfidence:
|
||||
results.length > 0 ? Math.round(totalConfidence / results.length) : 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const validateConfidence = async (
|
||||
confidence: ConfidenceScore,
|
||||
validatorFn: (factors: ReadonlyArray<ConfidenceFactor>) => Promise<{ validated: boolean; adjustment: number; notes: string }>
|
||||
validatorFn: (
|
||||
factors: ReadonlyArray<ConfidenceFactor>,
|
||||
) => Promise<{ validated: boolean; adjustment: number; notes: string }>,
|
||||
): Promise<ValidationResult> => {
|
||||
const result = await validatorFn(confidence.factors);
|
||||
|
||||
return {
|
||||
validated: result.validated,
|
||||
adjustedConfidence: Math.max(0, Math.min(100, confidence.value + result.adjustment)),
|
||||
adjustedConfidence: Math.max(
|
||||
0,
|
||||
Math.min(100, confidence.value + result.adjustment),
|
||||
),
|
||||
validatorNotes: result.notes,
|
||||
};
|
||||
};
|
||||
|
||||
export const formatConfidenceScore = (confidence: ConfidenceScore, showFactors: boolean = false): string => {
|
||||
export const formatConfidenceScore = (
|
||||
confidence: ConfidenceScore,
|
||||
showFactors: boolean = false,
|
||||
): string => {
|
||||
const levelColors: Record<ConfidenceLevel, string> = {
|
||||
low: "\x1b[90m",
|
||||
medium: "\x1b[33m",
|
||||
@@ -158,7 +198,10 @@ export const formatConfidenceScore = (confidence: ConfidenceScore, showFactors:
|
||||
|
||||
if (showFactors && confidence.factors.length > 0) {
|
||||
const factorLines = confidence.factors
|
||||
.map((f: ConfidenceFactor) => ` - ${f.name}: ${f.score}% (weight: ${f.weight})`)
|
||||
.map(
|
||||
(f: ConfidenceFactor) =>
|
||||
` - ${f.name}: ${f.score}% (weight: ${f.weight})`,
|
||||
)
|
||||
.join("\n");
|
||||
result += `\n${factorLines}`;
|
||||
}
|
||||
@@ -168,7 +211,7 @@ export const formatConfidenceScore = (confidence: ConfidenceScore, showFactors:
|
||||
|
||||
export const mergeConfidenceFactors = (
|
||||
existing: ReadonlyArray<ConfidenceFactor>,
|
||||
additional: ReadonlyArray<ConfidenceFactor>
|
||||
additional: ReadonlyArray<ConfidenceFactor>,
|
||||
): ReadonlyArray<ConfidenceFactor> => {
|
||||
const factorMap = new Map<string, ConfidenceFactor>();
|
||||
|
||||
@@ -191,7 +234,11 @@ export const mergeConfidenceFactors = (
|
||||
|
||||
export const adjustThreshold = (
|
||||
baseThreshold: number,
|
||||
context: { isCritical: boolean; isAutomated: boolean; userPreference?: number }
|
||||
context: {
|
||||
isCritical: boolean;
|
||||
isAutomated: boolean;
|
||||
userPreference?: number;
|
||||
},
|
||||
): number => {
|
||||
let threshold = context.userPreference ?? baseThreshold;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const PROJECT_MARKERS: Record<string, { type: string; language: string }> = {
|
||||
"pyproject.toml": { type: "Python", language: "Python" },
|
||||
"setup.py": { type: "Python", language: "Python" },
|
||||
"requirements.txt": { type: "Python", language: "Python" },
|
||||
"Gemfile": { type: "Ruby", language: "Ruby" },
|
||||
Gemfile: { type: "Ruby", language: "Ruby" },
|
||||
"composer.json": { type: "PHP", language: "PHP" },
|
||||
".csproj": { type: ".NET", language: "C#" },
|
||||
};
|
||||
@@ -73,7 +73,9 @@ const IGNORED_DIRS = new Set([
|
||||
"coverage",
|
||||
]);
|
||||
|
||||
const detectProjectType = (workingDir: string): { type: string; language: string } => {
|
||||
const detectProjectType = (
|
||||
workingDir: string,
|
||||
): { type: string; language: string } => {
|
||||
for (const [marker, info] of Object.entries(PROJECT_MARKERS)) {
|
||||
if (existsSync(join(workingDir, marker))) {
|
||||
return info;
|
||||
@@ -132,7 +134,12 @@ const getDirectoryStructure = (
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
entries.push(`${indent}${item}/`);
|
||||
const subEntries = getDirectoryStructure(fullPath, baseDir, depth + 1, maxDepth);
|
||||
const subEntries = getDirectoryStructure(
|
||||
fullPath,
|
||||
baseDir,
|
||||
depth + 1,
|
||||
maxDepth,
|
||||
);
|
||||
entries.push(...subEntries);
|
||||
} else if (depth < 2) {
|
||||
entries.push(`${indent}${item}`);
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
ToolCallMessage,
|
||||
ToolResultMessage,
|
||||
} from "@/types/agent";
|
||||
import { chat as providerChat } from "@providers/index";
|
||||
import { chat as providerChat } from "@providers/core/chat";
|
||||
import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index";
|
||||
import type { ToolContext, ToolCall, ToolResult } from "@/types/tools";
|
||||
import { initializePermissions } from "@services/core/permissions";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import type { Config, Provider } from "@/types/index";
|
||||
import type { Config, Provider } from "@/types/common";
|
||||
import { DIRS, FILES } from "@constants/paths";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import type { AgentType } from "@/types/common";
|
||||
import { ChatSession, ChatMessage, AgentType } from "@interafaces/index";
|
||||
import type { AgentType, ChatSession, ChatMessage } from "@/types/common";
|
||||
import type { SessionInfo } from "@/types/session";
|
||||
import { DIRS } from "@constants/paths";
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
* Manages user approval checkpoints during feature development.
|
||||
*/
|
||||
|
||||
import {
|
||||
PHASE_CHECKPOINTS,
|
||||
FEATURE_DEV_ERRORS,
|
||||
} from "@constants/feature-dev";
|
||||
import { PHASE_CHECKPOINTS, FEATURE_DEV_ERRORS } from "@constants/feature-dev";
|
||||
import type {
|
||||
FeatureDevPhase,
|
||||
FeatureDevState,
|
||||
@@ -80,7 +77,9 @@ const buildCheckpointSummary = (
|
||||
},
|
||||
|
||||
review: () => {
|
||||
const issues = state.reviewFindings.filter((f) => f.type === "issue").length;
|
||||
const issues = state.reviewFindings.filter(
|
||||
(f) => f.type === "issue",
|
||||
).length;
|
||||
const suggestions = state.reviewFindings.filter(
|
||||
(f) => f.type === "suggestion",
|
||||
).length;
|
||||
@@ -145,7 +144,7 @@ export const processCheckpointDecision = (
|
||||
> = {
|
||||
approve: () => ({ proceed: true }),
|
||||
reject: () => ({ proceed: false, action: "rejected" }),
|
||||
modify: () => ({ proceed: false, action: "modify", }),
|
||||
modify: () => ({ proceed: false, action: "modify" }),
|
||||
skip: () => ({ proceed: true, action: "skipped" }),
|
||||
abort: () => ({ proceed: false, action: "aborted" }),
|
||||
};
|
||||
|
||||
@@ -4,14 +4,8 @@
|
||||
* Builds context for each phase of feature development.
|
||||
*/
|
||||
|
||||
import {
|
||||
PHASE_PROMPTS,
|
||||
PHASE_DESCRIPTIONS,
|
||||
} from "@constants/feature-dev";
|
||||
import type {
|
||||
FeatureDevPhase,
|
||||
FeatureDevState,
|
||||
} from "@/types/feature-dev";
|
||||
import { PHASE_PROMPTS, PHASE_DESCRIPTIONS } from "@constants/feature-dev";
|
||||
import type { FeatureDevPhase, FeatureDevState } from "@/types/feature-dev";
|
||||
|
||||
/**
|
||||
* Build the full context for a phase execution
|
||||
@@ -220,7 +214,9 @@ const buildStateContext = (
|
||||
|
||||
// Test status
|
||||
if (state.testResults) {
|
||||
const status = state.testResults.passed ? "✓ All tests passing" : "✗ Tests failing";
|
||||
const status = state.testResults.passed
|
||||
? "✓ All tests passing"
|
||||
: "✗ Tests failing";
|
||||
lines.push(`### Test Status: ${status}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,7 @@ const runGitCommand = (
|
||||
/**
|
||||
* Fetch PR details using gh CLI
|
||||
*/
|
||||
export const fetchPR = async (
|
||||
parts: PRUrlParts,
|
||||
): Promise<GitHubPR | null> => {
|
||||
export const fetchPR = async (parts: PRUrlParts): Promise<GitHubPR | null> => {
|
||||
const { owner, repo, prNumber } = parts;
|
||||
|
||||
const result = await executeGHCommand([
|
||||
|
||||
@@ -48,7 +48,9 @@ export const formatPRComments = (comments: GitHubPRComment[]): string => {
|
||||
for (const comment of comments) {
|
||||
lines.push(`### Comment by ${comment.author}`);
|
||||
if (comment.path) {
|
||||
lines.push(`**File:** ${comment.path}${comment.line ? `:${comment.line}` : ""}`);
|
||||
lines.push(
|
||||
`**File:** ${comment.path}${comment.line ? `:${comment.line}` : ""}`,
|
||||
);
|
||||
}
|
||||
lines.push(`**Date:** ${new Date(comment.createdAt).toLocaleDateString()}`);
|
||||
lines.push("");
|
||||
@@ -90,7 +92,9 @@ export const formatPRReviews = (reviews: GitHubPRReview[]): string => {
|
||||
for (const review of reviews) {
|
||||
const emoji = stateEmojis[review.state] || "";
|
||||
lines.push(`### ${emoji} ${review.state} by ${review.author}`);
|
||||
lines.push(`**Date:** ${new Date(review.submittedAt).toLocaleDateString()}`);
|
||||
lines.push(
|
||||
`**Date:** ${new Date(review.submittedAt).toLocaleDateString()}`,
|
||||
);
|
||||
|
||||
if (review.body) {
|
||||
lines.push("");
|
||||
@@ -154,7 +158,9 @@ export const formatCommentForSolving = (comment: GitHubPRComment): string => {
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("Please address this comment by making the necessary changes to the code.");
|
||||
lines.push(
|
||||
"Please address this comment by making the necessary changes to the code.",
|
||||
);
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
@@ -167,17 +173,16 @@ export const formatPendingComments = (comments: GitHubPRComment[]): string => {
|
||||
return "No pending comments to address.";
|
||||
}
|
||||
|
||||
const lines: string[] = [
|
||||
`## ${comments.length} Comment(s) to Address`,
|
||||
"",
|
||||
];
|
||||
const lines: string[] = [`## ${comments.length} Comment(s) to Address`, ""];
|
||||
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
const comment = comments[i];
|
||||
lines.push(`### ${i + 1}. Comment by ${comment.author}`);
|
||||
|
||||
if (comment.path) {
|
||||
lines.push(`**File:** ${comment.path}${comment.line ? `:${comment.line}` : ""}`);
|
||||
lines.push(
|
||||
`**File:** ${comment.path}${comment.line ? `:${comment.line}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
|
||||
@@ -46,7 +46,9 @@ const hooksCache: HooksCache = {
|
||||
/**
|
||||
* Load hooks configuration from a file
|
||||
*/
|
||||
const loadHooksFromFile = async (filePath: string): Promise<HookDefinition[]> => {
|
||||
const loadHooksFromFile = async (
|
||||
filePath: string,
|
||||
): Promise<HookDefinition[]> => {
|
||||
try {
|
||||
await access(filePath, constants.R_OK);
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
@@ -57,7 +59,7 @@ const loadHooksFromFile = async (filePath: string): Promise<HookDefinition[]> =>
|
||||
}
|
||||
|
||||
return config.hooks.filter(
|
||||
(hook) => hook.enabled !== false && hook.event && hook.script
|
||||
(hook) => hook.enabled !== false && hook.event && hook.script,
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
@@ -117,7 +119,7 @@ const resolveScriptPath = (script: string, workingDir: string): string => {
|
||||
const executeHookScript = async (
|
||||
hook: HookDefinition,
|
||||
input: HookInput,
|
||||
workingDir: string
|
||||
workingDir: string,
|
||||
): Promise<HookResult> => {
|
||||
const scriptPath = resolveScriptPath(hook.script, workingDir);
|
||||
const timeout = hook.timeout ?? DEFAULT_HOOK_TIMEOUT;
|
||||
@@ -201,7 +203,8 @@ const executeHookScript = async (
|
||||
} else if (exitCode === HOOK_EXIT_CODES.BLOCK) {
|
||||
resolvePromise({
|
||||
action: "block",
|
||||
message: stderr.trim() || `Blocked by hook: ${hook.name || hook.script}`,
|
||||
message:
|
||||
stderr.trim() || `Blocked by hook: ${hook.name || hook.script}`,
|
||||
});
|
||||
} else {
|
||||
resolvePromise({
|
||||
@@ -231,7 +234,7 @@ const executeHookScript = async (
|
||||
const executeHooks = async (
|
||||
event: HookEventType,
|
||||
input: HookInput,
|
||||
workingDir: string
|
||||
workingDir: string,
|
||||
): Promise<HookResult> => {
|
||||
const hooks = getHooksForEvent(event);
|
||||
|
||||
@@ -288,7 +291,7 @@ export const executePreToolUseHooks = async (
|
||||
sessionId: string,
|
||||
toolName: string,
|
||||
toolArgs: Record<string, unknown>,
|
||||
workingDir: string
|
||||
workingDir: string,
|
||||
): Promise<HookResult> => {
|
||||
if (!hooksCache.loaded) {
|
||||
await loadHooks(workingDir);
|
||||
@@ -312,7 +315,7 @@ export const executePostToolUseHooks = async (
|
||||
toolName: string,
|
||||
toolArgs: Record<string, unknown>,
|
||||
result: ToolResult,
|
||||
workingDir: string
|
||||
workingDir: string,
|
||||
): Promise<void> => {
|
||||
if (!hooksCache.loaded) {
|
||||
await loadHooks(workingDir);
|
||||
@@ -341,7 +344,7 @@ export const executeSessionStartHooks = async (
|
||||
sessionId: string,
|
||||
workingDir: string,
|
||||
provider: string,
|
||||
model: string
|
||||
model: string,
|
||||
): Promise<void> => {
|
||||
if (!hooksCache.loaded) {
|
||||
await loadHooks(workingDir);
|
||||
@@ -364,7 +367,7 @@ export const executeSessionEndHooks = async (
|
||||
sessionId: string,
|
||||
workingDir: string,
|
||||
duration: number,
|
||||
messageCount: number
|
||||
messageCount: number,
|
||||
): Promise<void> => {
|
||||
if (!hooksCache.loaded) {
|
||||
await loadHooks(workingDir);
|
||||
@@ -386,7 +389,7 @@ export const executeSessionEndHooks = async (
|
||||
export const executeUserPromptSubmitHooks = async (
|
||||
sessionId: string,
|
||||
prompt: string,
|
||||
workingDir: string
|
||||
workingDir: string,
|
||||
): Promise<HookResult> => {
|
||||
if (!hooksCache.loaded) {
|
||||
await loadHooks(workingDir);
|
||||
@@ -407,7 +410,7 @@ export const executeUserPromptSubmitHooks = async (
|
||||
export const executeStopHooks = async (
|
||||
sessionId: string,
|
||||
workingDir: string,
|
||||
reason: "interrupt" | "complete" | "error"
|
||||
reason: "interrupt" | "complete" | "error",
|
||||
): Promise<void> => {
|
||||
if (!hooksCache.loaded) {
|
||||
await loadHooks(workingDir);
|
||||
|
||||
@@ -49,7 +49,10 @@ export interface DocumentSymbol {
|
||||
}
|
||||
|
||||
export interface Hover {
|
||||
contents: string | { kind: string; value: string } | Array<string | { kind: string; value: string }>;
|
||||
contents:
|
||||
| string
|
||||
| { kind: string; value: string }
|
||||
| Array<string | { kind: string; value: string }>;
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
@@ -163,7 +166,10 @@ export class LSPClient extends EventEmitter {
|
||||
|
||||
private handleNotification(method: string, params: unknown): void {
|
||||
if (method === "textDocument/publishDiagnostics") {
|
||||
const { uri, diagnostics } = params as { uri: string; diagnostics: Diagnostic[] };
|
||||
const { uri, diagnostics } = params as {
|
||||
uri: string;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
this.diagnosticsMap.set(uri, diagnostics);
|
||||
this.emit("diagnostics", uri, diagnostics);
|
||||
}
|
||||
@@ -213,7 +219,9 @@ export class LSPClient extends EventEmitter {
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
const result = await this.request<{ capabilities: Record<string, unknown> }>("initialize", {
|
||||
const result = await this.request<{
|
||||
capabilities: Record<string, unknown>;
|
||||
}>("initialize", {
|
||||
processId: process.pid,
|
||||
rootUri: `file://${this.root}`,
|
||||
rootPath: this.root,
|
||||
@@ -319,45 +327,60 @@ export class LSPClient extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async getDefinition(filePath: string, position: Position): Promise<Location | Location[] | null> {
|
||||
async getDefinition(
|
||||
filePath: string,
|
||||
position: Position,
|
||||
): Promise<Location | Location[] | null> {
|
||||
const uri = `file://${filePath}`;
|
||||
|
||||
try {
|
||||
return await this.request<Location | Location[] | null>("textDocument/definition", {
|
||||
textDocument: { uri },
|
||||
position,
|
||||
});
|
||||
return await this.request<Location | Location[] | null>(
|
||||
"textDocument/definition",
|
||||
{
|
||||
textDocument: { uri },
|
||||
position,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getReferences(filePath: string, position: Position, includeDeclaration = true): Promise<Location[]> {
|
||||
async getReferences(
|
||||
filePath: string,
|
||||
position: Position,
|
||||
includeDeclaration = true,
|
||||
): Promise<Location[]> {
|
||||
const uri = `file://${filePath}`;
|
||||
|
||||
try {
|
||||
const result = await this.request<Location[] | null>("textDocument/references", {
|
||||
textDocument: { uri },
|
||||
position,
|
||||
context: { includeDeclaration },
|
||||
});
|
||||
const result = await this.request<Location[] | null>(
|
||||
"textDocument/references",
|
||||
{
|
||||
textDocument: { uri },
|
||||
position,
|
||||
context: { includeDeclaration },
|
||||
},
|
||||
);
|
||||
return result ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getCompletions(filePath: string, position: Position): Promise<CompletionItem[]> {
|
||||
async getCompletions(
|
||||
filePath: string,
|
||||
position: Position,
|
||||
): Promise<CompletionItem[]> {
|
||||
const uri = `file://${filePath}`;
|
||||
|
||||
try {
|
||||
const result = await this.request<{ items: CompletionItem[] } | CompletionItem[] | null>(
|
||||
"textDocument/completion",
|
||||
{
|
||||
textDocument: { uri },
|
||||
position,
|
||||
},
|
||||
);
|
||||
const result = await this.request<
|
||||
{ items: CompletionItem[] } | CompletionItem[] | null
|
||||
>("textDocument/completion", {
|
||||
textDocument: { uri },
|
||||
position,
|
||||
});
|
||||
|
||||
if (!result) return [];
|
||||
return Array.isArray(result) ? result : result.items;
|
||||
@@ -370,9 +393,12 @@ export class LSPClient extends EventEmitter {
|
||||
const uri = `file://${filePath}`;
|
||||
|
||||
try {
|
||||
const result = await this.request<DocumentSymbol[] | null>("textDocument/documentSymbol", {
|
||||
textDocument: { uri },
|
||||
});
|
||||
const result = await this.request<DocumentSymbol[] | null>(
|
||||
"textDocument/documentSymbol",
|
||||
{
|
||||
textDocument: { uri },
|
||||
},
|
||||
);
|
||||
return result ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
|
||||
@@ -152,7 +152,10 @@ export const openFile = async (filePath: string): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const updateFile = async (filePath: string, content: string): Promise<void> => {
|
||||
export const updateFile = async (
|
||||
filePath: string,
|
||||
content: string,
|
||||
): Promise<void> => {
|
||||
const absolutePath = path.resolve(filePath);
|
||||
const clients = await getClientsForFile(absolutePath);
|
||||
|
||||
@@ -176,7 +179,10 @@ export const closeFile = async (filePath: string): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getHover = async (filePath: string, position: Position): Promise<Hover | null> => {
|
||||
export const getHover = async (
|
||||
filePath: string,
|
||||
position: Position,
|
||||
): Promise<Hover | null> => {
|
||||
const absolutePath = path.resolve(filePath);
|
||||
const clients = await getClientsForFile(absolutePath);
|
||||
|
||||
@@ -213,7 +219,11 @@ export const getReferences = async (
|
||||
|
||||
const allRefs: Location[] = [];
|
||||
for (const client of clients) {
|
||||
const refs = await client.getReferences(absolutePath, position, includeDeclaration);
|
||||
const refs = await client.getReferences(
|
||||
absolutePath,
|
||||
position,
|
||||
includeDeclaration,
|
||||
);
|
||||
allRefs.push(...refs);
|
||||
}
|
||||
|
||||
@@ -243,7 +253,9 @@ export const getCompletions = async (
|
||||
return allCompletions;
|
||||
};
|
||||
|
||||
export const getDocumentSymbols = async (filePath: string): Promise<DocumentSymbol[]> => {
|
||||
export const getDocumentSymbols = async (
|
||||
filePath: string,
|
||||
): Promise<DocumentSymbol[]> => {
|
||||
const absolutePath = path.resolve(filePath);
|
||||
const clients = await getClientsForFile(absolutePath);
|
||||
|
||||
@@ -255,7 +267,9 @@ export const getDocumentSymbols = async (filePath: string): Promise<DocumentSymb
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getDiagnostics = (filePath?: string): Map<string, Diagnostic[]> => {
|
||||
export const getDiagnostics = (
|
||||
filePath?: string,
|
||||
): Map<string, Diagnostic[]> => {
|
||||
const allDiagnostics = new Map<string, Diagnostic[]>();
|
||||
|
||||
for (const client of state.clients.values()) {
|
||||
@@ -278,7 +292,9 @@ export const getStatus = (): {
|
||||
connected: Array<{ serverId: string; root: string }>;
|
||||
broken: string[];
|
||||
} => {
|
||||
const connected = Array.from(state.clients.values()).map((client) => client.getInfo());
|
||||
const connected = Array.from(state.clients.values()).map((client) =>
|
||||
client.getInfo(),
|
||||
);
|
||||
const broken = Array.from(state.broken);
|
||||
|
||||
return { connected, broken };
|
||||
@@ -303,7 +319,11 @@ export const shutdown = (): void => {
|
||||
};
|
||||
|
||||
export const onDiagnostics = (
|
||||
callback: (data: { uri: string; diagnostics: Diagnostic[]; serverId: string }) => void,
|
||||
callback: (data: {
|
||||
uri: string;
|
||||
diagnostics: Diagnostic[];
|
||||
serverId: string;
|
||||
}) => void,
|
||||
): (() => void) => {
|
||||
events.on("diagnostics", callback);
|
||||
return () => events.off("diagnostics", callback);
|
||||
|
||||
@@ -166,9 +166,13 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
||||
export const getLanguageId = (filePath: string): string | null => {
|
||||
const ext = filePath.includes(".")
|
||||
? "." + filePath.split(".").pop()
|
||||
: filePath.split("/").pop() ?? "";
|
||||
: (filePath.split("/").pop() ?? "");
|
||||
|
||||
return LANGUAGE_EXTENSIONS[ext] ?? LANGUAGE_EXTENSIONS[filePath.split("/").pop() ?? ""] ?? null;
|
||||
return (
|
||||
LANGUAGE_EXTENSIONS[ext] ??
|
||||
LANGUAGE_EXTENSIONS[filePath.split("/").pop() ?? ""] ??
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
export const getExtensionsForLanguage = (languageId: string): string[] => {
|
||||
|
||||
@@ -54,8 +54,12 @@ const findProjectRoot = async (
|
||||
|
||||
const findBinary = async (name: string): Promise<string | null> => {
|
||||
try {
|
||||
const command = process.platform === "win32" ? `where ${name}` : `which ${name}`;
|
||||
const result = execSync(command, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
||||
const command =
|
||||
process.platform === "win32" ? `where ${name}` : `which ${name}`;
|
||||
const result = execSync(command, {
|
||||
encoding: "utf-8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
return result.trim().split("\n")[0] || null;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -83,7 +87,12 @@ export const SERVERS: Record<string, ServerInfo> = {
|
||||
id: "python",
|
||||
name: "Pyright",
|
||||
extensions: [".py", ".pyi"],
|
||||
rootPatterns: ["pyproject.toml", "setup.py", "requirements.txt", "pyrightconfig.json"],
|
||||
rootPatterns: [
|
||||
"pyproject.toml",
|
||||
"setup.py",
|
||||
"requirements.txt",
|
||||
"pyrightconfig.json",
|
||||
],
|
||||
command: "pyright-langserver",
|
||||
args: ["--stdio"],
|
||||
},
|
||||
@@ -160,7 +169,12 @@ export const SERVERS: Record<string, ServerInfo> = {
|
||||
id: "eslint",
|
||||
name: "ESLint Language Server",
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
||||
rootPatterns: [".eslintrc", ".eslintrc.js", ".eslintrc.json", "eslint.config.js"],
|
||||
rootPatterns: [
|
||||
".eslintrc",
|
||||
".eslintrc.js",
|
||||
".eslintrc.json",
|
||||
"eslint.config.js",
|
||||
],
|
||||
command: "vscode-eslint-language-server",
|
||||
args: ["--stdio"],
|
||||
},
|
||||
@@ -212,8 +226,7 @@ export const getServersForFile = (filePath: string): ServerInfo[] => {
|
||||
|
||||
return Object.values(SERVERS).filter((server) => {
|
||||
return (
|
||||
server.extensions.includes(ext) ||
|
||||
server.extensions.includes(fileName)
|
||||
server.extensions.includes(ext) || server.extensions.includes(fileName)
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -249,7 +262,9 @@ export const spawnServer = async (
|
||||
return { process: proc };
|
||||
};
|
||||
|
||||
export const isServerAvailable = async (server: ServerInfo): Promise<boolean> => {
|
||||
export const isServerAvailable = async (
|
||||
server: ServerInfo,
|
||||
): Promise<boolean> => {
|
||||
const binary = await findBinary(server.command);
|
||||
return binary !== null;
|
||||
};
|
||||
|
||||
@@ -154,7 +154,7 @@ const mapCategory = (category: string): MCPServerCategory => {
|
||||
* Get all servers (curated + external)
|
||||
*/
|
||||
export const getAllServers = async (
|
||||
forceRefresh = false
|
||||
forceRefresh = false,
|
||||
): Promise<MCPRegistryServer[]> => {
|
||||
// Check in-memory cache first
|
||||
if (!forceRefresh && registryCache && isCacheValid(registryCache)) {
|
||||
@@ -202,7 +202,7 @@ export const getCuratedServers = (): MCPRegistryServer[] => {
|
||||
* Search for MCP servers
|
||||
*/
|
||||
export const searchServers = async (
|
||||
options: MCPSearchOptions = {}
|
||||
options: MCPSearchOptions = {},
|
||||
): Promise<MCPSearchResult> => {
|
||||
const {
|
||||
query = "",
|
||||
@@ -227,7 +227,9 @@ export const searchServers = async (
|
||||
server.description,
|
||||
server.author,
|
||||
...server.tags,
|
||||
].join(" ").toLowerCase();
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
|
||||
return searchableText.includes(lowerQuery);
|
||||
});
|
||||
@@ -243,9 +245,9 @@ export const searchServers = async (
|
||||
filtered = filtered.filter((server) =>
|
||||
tags.some((tag) =>
|
||||
server.tags.some((serverTag) =>
|
||||
serverTag.toLowerCase().includes(tag.toLowerCase())
|
||||
)
|
||||
)
|
||||
serverTag.toLowerCase().includes(tag.toLowerCase()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,10 +257,14 @@ export const searchServers = async (
|
||||
}
|
||||
|
||||
// Sort
|
||||
const sortFunctions: Record<string, (a: MCPRegistryServer, b: MCPRegistryServer) => number> = {
|
||||
const sortFunctions: Record<
|
||||
string,
|
||||
(a: MCPRegistryServer, b: MCPRegistryServer) => number
|
||||
> = {
|
||||
popularity: (a, b) => b.popularity - a.popularity,
|
||||
name: (a, b) => a.name.localeCompare(b.name),
|
||||
updated: (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
updated: (a, b) =>
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
};
|
||||
|
||||
filtered.sort(sortFunctions[sortBy] || sortFunctions.popularity);
|
||||
@@ -279,7 +285,7 @@ export const searchServers = async (
|
||||
* Get server by ID
|
||||
*/
|
||||
export const getServerById = async (
|
||||
id: string
|
||||
id: string,
|
||||
): Promise<MCPRegistryServer | undefined> => {
|
||||
const allServers = await getAllServers();
|
||||
return allServers.find((server) => server.id === id);
|
||||
@@ -289,7 +295,7 @@ export const getServerById = async (
|
||||
* Get servers by category
|
||||
*/
|
||||
export const getServersByCategory = async (
|
||||
category: MCPServerCategory
|
||||
category: MCPServerCategory,
|
||||
): Promise<MCPRegistryServer[]> => {
|
||||
const allServers = await getAllServers();
|
||||
return allServers.filter((server) => server.category === category);
|
||||
@@ -300,9 +306,10 @@ export const getServersByCategory = async (
|
||||
*/
|
||||
export const isServerInstalled = (serverId: string): boolean => {
|
||||
const instances = getServerInstances();
|
||||
return Array.from(instances.values()).some((instance) =>
|
||||
instance.config.name === serverId ||
|
||||
instance.config.name.toLowerCase() === serverId.toLowerCase()
|
||||
return Array.from(instances.values()).some(
|
||||
(instance) =>
|
||||
instance.config.name === serverId ||
|
||||
instance.config.name.toLowerCase() === serverId.toLowerCase(),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -315,7 +322,7 @@ export const installServer = async (
|
||||
global?: boolean;
|
||||
connect?: boolean;
|
||||
customArgs?: string[];
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<MCPInstallResult> => {
|
||||
const { global = false, connect = true, customArgs } = options;
|
||||
|
||||
@@ -339,7 +346,7 @@ export const installServer = async (
|
||||
transport: server.transport,
|
||||
enabled: true,
|
||||
},
|
||||
global
|
||||
global,
|
||||
);
|
||||
|
||||
let connected = false;
|
||||
@@ -363,7 +370,10 @@ export const installServer = async (
|
||||
return {
|
||||
success: false,
|
||||
serverName: server.id,
|
||||
error: error instanceof Error ? error.message : MCP_REGISTRY_ERRORS.INSTALL_FAILED,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: MCP_REGISTRY_ERRORS.INSTALL_FAILED,
|
||||
connected: false,
|
||||
};
|
||||
}
|
||||
@@ -378,7 +388,7 @@ export const installServerById = async (
|
||||
global?: boolean;
|
||||
connect?: boolean;
|
||||
customArgs?: string[];
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<MCPInstallResult> => {
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
@@ -398,12 +408,10 @@ export const installServerById = async (
|
||||
* Get popular servers
|
||||
*/
|
||||
export const getPopularServers = async (
|
||||
limit = 10
|
||||
limit = 10,
|
||||
): Promise<MCPRegistryServer[]> => {
|
||||
const allServers = await getAllServers();
|
||||
return allServers
|
||||
.sort((a, b) => b.popularity - a.popularity)
|
||||
.slice(0, limit);
|
||||
return allServers.sort((a, b) => b.popularity - a.popularity).slice(0, limit);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,11 +39,7 @@ export const MODEL_TIER_MAPPING: Record<ModelTier, string[]> = {
|
||||
"gpt-4.1",
|
||||
],
|
||||
// Thorough tier: Best quality, higher cost (3x multiplier)
|
||||
thorough: [
|
||||
"claude-opus-4.5",
|
||||
"gpt-5.2-codex",
|
||||
"gpt-5.1-codex-max",
|
||||
],
|
||||
thorough: ["claude-opus-4.5", "gpt-5.2-codex", "gpt-5.1-codex-max"],
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,14 +26,18 @@ let agentRegistry: Map<string, AgentDefinition> = new Map();
|
||||
/**
|
||||
* Set the agent registry (called during initialization)
|
||||
*/
|
||||
export const setAgentRegistry = (registry: Map<string, AgentDefinition>): void => {
|
||||
export const setAgentRegistry = (
|
||||
registry: Map<string, AgentDefinition>,
|
||||
): void => {
|
||||
agentRegistry = registry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get agent definition by name
|
||||
*/
|
||||
export const getAgentDefinition = (name: string): AgentDefinition | undefined => {
|
||||
export const getAgentDefinition = (
|
||||
name: string,
|
||||
): AgentDefinition | undefined => {
|
||||
return agentRegistry.get(name);
|
||||
};
|
||||
|
||||
@@ -50,7 +54,11 @@ export const createAgentInstance = (
|
||||
|
||||
const activeCount = multiAgentStore.getActiveInstances().length;
|
||||
if (activeCount >= MULTI_AGENT_LIMITS.maxConcurrentRequests) {
|
||||
return { error: MULTI_AGENT_ERRORS.MAX_CONCURRENT_EXCEEDED(MULTI_AGENT_LIMITS.maxConcurrentRequests) };
|
||||
return {
|
||||
error: MULTI_AGENT_ERRORS.MAX_CONCURRENT_EXCEEDED(
|
||||
MULTI_AGENT_LIMITS.maxConcurrentRequests,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const conversation: AgentConversation = {
|
||||
@@ -122,7 +130,19 @@ export const completeAgent = (
|
||||
...(result.success
|
||||
? { result, timestamp: Date.now() }
|
||||
: { error: result.error ?? "Unknown error", timestamp: Date.now() }),
|
||||
} as { type: "agent_completed"; agentId: string; result: AgentExecutionResult; timestamp: number } | { type: "agent_error"; agentId: string; error: string; timestamp: number });
|
||||
} as
|
||||
| {
|
||||
type: "agent_completed";
|
||||
agentId: string;
|
||||
result: AgentExecutionResult;
|
||||
timestamp: number;
|
||||
}
|
||||
| {
|
||||
type: "agent_error";
|
||||
agentId: string;
|
||||
error: string;
|
||||
timestamp: number;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,10 +11,7 @@ import type {
|
||||
AgentInstance,
|
||||
} from "@/types/multi-agent";
|
||||
import { multiAgentStore } from "@stores/core/multi-agent-store";
|
||||
import {
|
||||
MULTI_AGENT_ERRORS,
|
||||
FILE_LOCK,
|
||||
} from "@/constants/multi-agent";
|
||||
import { MULTI_AGENT_ERRORS, FILE_LOCK } from "@/constants/multi-agent";
|
||||
import {
|
||||
pauseAgentForConflict,
|
||||
resumeAgent,
|
||||
@@ -28,10 +25,13 @@ const fileLocks: Map<string, string> = new Map(); // filePath -> agentId
|
||||
/**
|
||||
* Pending lock requests
|
||||
*/
|
||||
const pendingLocks: Map<string, Array<{
|
||||
agentId: string;
|
||||
resolve: (acquired: boolean) => void;
|
||||
}>> = new Map();
|
||||
const pendingLocks: Map<
|
||||
string,
|
||||
Array<{
|
||||
agentId: string;
|
||||
resolve: (acquired: boolean) => void;
|
||||
}>
|
||||
> = new Map();
|
||||
|
||||
/**
|
||||
* Acquire a file lock for an agent
|
||||
@@ -256,7 +256,10 @@ const waitForAgentCompletion = (agentId: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
const agent = multiAgentStore.getState().instances.get(agentId);
|
||||
if (!agent || ["completed", "error", "cancelled"].includes(agent.status)) {
|
||||
if (
|
||||
!agent ||
|
||||
["completed", "error", "cancelled"].includes(agent.status)
|
||||
) {
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
|
||||
@@ -100,7 +100,9 @@ const validateRequest = (
|
||||
}
|
||||
|
||||
if (request.agents.length > MULTI_AGENT_LIMITS.maxAgentsPerRequest) {
|
||||
return MULTI_AGENT_ERRORS.MAX_AGENTS_EXCEEDED(MULTI_AGENT_LIMITS.maxAgentsPerRequest);
|
||||
return MULTI_AGENT_ERRORS.MAX_AGENTS_EXCEEDED(
|
||||
MULTI_AGENT_LIMITS.maxAgentsPerRequest,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate each agent config
|
||||
@@ -146,7 +148,8 @@ const executeParallel = async (
|
||||
results: AgentInstance[],
|
||||
conflicts: FileConflict[],
|
||||
): Promise<void> => {
|
||||
const maxConcurrent = request.maxConcurrent ?? MULTI_AGENT_DEFAULTS.maxConcurrent;
|
||||
const maxConcurrent =
|
||||
request.maxConcurrent ?? MULTI_AGENT_DEFAULTS.maxConcurrent;
|
||||
const chunks = chunkArray(request.agents, maxConcurrent);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
@@ -182,7 +185,8 @@ const executeAdaptive = async (
|
||||
results: AgentInstance[],
|
||||
conflicts: FileConflict[],
|
||||
): Promise<void> => {
|
||||
const maxConcurrent = request.maxConcurrent ?? MULTI_AGENT_DEFAULTS.maxConcurrent;
|
||||
const maxConcurrent =
|
||||
request.maxConcurrent ?? MULTI_AGENT_DEFAULTS.maxConcurrent;
|
||||
let conflictCount = 0;
|
||||
let useSequential = false;
|
||||
|
||||
@@ -260,12 +264,7 @@ const executeSingleAgent = async (
|
||||
const instance = instanceOrError;
|
||||
|
||||
// Create tool context
|
||||
createToolContext(
|
||||
instance.id,
|
||||
process.cwd(),
|
||||
config.contextFiles,
|
||||
[],
|
||||
);
|
||||
createToolContext(instance.id, process.cwd(), config.contextFiles, []);
|
||||
|
||||
// Start agent
|
||||
startAgent(instance.id);
|
||||
@@ -353,7 +352,8 @@ const aggregateResults = (
|
||||
cancelled,
|
||||
conflicts,
|
||||
totalDuration: Date.now() - startTime,
|
||||
aggregatedOutput: outputs.length > 0 ? outputs.join("\n\n---\n\n") : undefined,
|
||||
aggregatedOutput:
|
||||
outputs.length > 0 ? outputs.join("\n\n---\n\n") : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -51,10 +51,7 @@ export const getToolContext = (agentId: string): AgentToolContext | null => {
|
||||
/**
|
||||
* Check if a path is allowed for an agent
|
||||
*/
|
||||
export const isPathAllowed = (
|
||||
agentId: string,
|
||||
filePath: string,
|
||||
): boolean => {
|
||||
export const isPathAllowed = (agentId: string, filePath: string): boolean => {
|
||||
const context = activeContexts.get(agentId);
|
||||
if (!context) return false;
|
||||
|
||||
@@ -101,13 +98,21 @@ export const requestWriteAccess = async (
|
||||
// Detect conflicts with other agents
|
||||
const conflict = detectConflict(agentId, filePath);
|
||||
if (conflict) {
|
||||
return { granted: false, conflict: true, reason: "File locked by another agent" };
|
||||
return {
|
||||
granted: false,
|
||||
conflict: true,
|
||||
reason: "File locked by another agent",
|
||||
};
|
||||
}
|
||||
|
||||
// Acquire file lock
|
||||
const acquired = await acquireFileLock(agentId, filePath);
|
||||
if (!acquired) {
|
||||
return { granted: false, conflict: true, reason: "Could not acquire file lock" };
|
||||
return {
|
||||
granted: false,
|
||||
conflict: true,
|
||||
reason: "Could not acquire file lock",
|
||||
};
|
||||
}
|
||||
|
||||
// Track locked file
|
||||
@@ -118,10 +123,7 @@ export const requestWriteAccess = async (
|
||||
/**
|
||||
* Record a file modification
|
||||
*/
|
||||
export const recordModification = (
|
||||
agentId: string,
|
||||
filePath: string,
|
||||
): void => {
|
||||
export const recordModification = (agentId: string, filePath: string): void => {
|
||||
const context = activeContexts.get(agentId);
|
||||
if (!context) return;
|
||||
|
||||
@@ -132,10 +134,7 @@ export const recordModification = (
|
||||
/**
|
||||
* Release write access to a file
|
||||
*/
|
||||
export const releaseWriteAccess = (
|
||||
agentId: string,
|
||||
filePath: string,
|
||||
): void => {
|
||||
export const releaseWriteAccess = (agentId: string, filePath: string): void => {
|
||||
const context = activeContexts.get(agentId);
|
||||
if (!context) return;
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
* and task types. Read-only tasks don't conflict with each other.
|
||||
*/
|
||||
|
||||
import { CONFLICT_CONFIG, READ_ONLY_TASK_TYPES, MODIFYING_TASK_TYPES } from "@constants/parallel";
|
||||
import {
|
||||
CONFLICT_CONFIG,
|
||||
READ_ONLY_TASK_TYPES,
|
||||
MODIFYING_TASK_TYPES,
|
||||
} from "@constants/parallel";
|
||||
import type {
|
||||
ParallelTask,
|
||||
ConflictCheckResult,
|
||||
@@ -143,7 +147,9 @@ export const checkConflicts = (task: ParallelTask): ConflictCheckResult => {
|
||||
const hasConflict = conflictingTaskIds.length > 0;
|
||||
|
||||
// Suggest resolution
|
||||
const resolution = hasConflict ? suggestResolution(task, conflictingTaskIds) : undefined;
|
||||
const resolution = hasConflict
|
||||
? suggestResolution(task, conflictingTaskIds)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
hasConflict,
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
* Coordinates conflict detection, resource management, and result aggregation.
|
||||
*/
|
||||
|
||||
import { PARALLEL_DEFAULTS, PARALLEL_ERRORS, TASK_TIMEOUTS } from "@constants/parallel";
|
||||
import {
|
||||
PARALLEL_DEFAULTS,
|
||||
PARALLEL_ERRORS,
|
||||
TASK_TIMEOUTS,
|
||||
} from "@constants/parallel";
|
||||
import {
|
||||
registerActiveTask,
|
||||
unregisterActiveTask,
|
||||
@@ -50,7 +54,10 @@ const executeTask = async <TInput, TOutput>(
|
||||
options: ParallelExecutorOptions,
|
||||
): Promise<ParallelExecutionResult<TOutput>> => {
|
||||
const startedAt = Date.now();
|
||||
const timeout = task.timeout ?? TASK_TIMEOUTS[task.type] ?? PARALLEL_DEFAULTS.defaultTimeout;
|
||||
const timeout =
|
||||
task.timeout ??
|
||||
TASK_TIMEOUTS[task.type] ??
|
||||
PARALLEL_DEFAULTS.defaultTimeout;
|
||||
|
||||
try {
|
||||
// Notify task start
|
||||
@@ -87,7 +94,10 @@ const executeTask = async <TInput, TOutput>(
|
||||
completedAt,
|
||||
};
|
||||
|
||||
options.onTaskError?.(task, error instanceof Error ? error : new Error(String(error)));
|
||||
options.onTaskError?.(
|
||||
task,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
return executionResult;
|
||||
}
|
||||
};
|
||||
@@ -134,7 +144,10 @@ export const executeParallel = async <TInput, TOutput>(
|
||||
|
||||
// Track results
|
||||
const results: ParallelExecutionResult<TOutput>[] = [];
|
||||
const pendingTasks = new Map<string, Promise<ParallelExecutionResult<TOutput>>>();
|
||||
const pendingTasks = new Map<
|
||||
string,
|
||||
Promise<ParallelExecutionResult<TOutput>>
|
||||
>();
|
||||
|
||||
// Check if executor was aborted
|
||||
const checkAbort = (): boolean => {
|
||||
@@ -209,9 +222,15 @@ const executeWithConflictHandling = async <TInput, TOutput>(
|
||||
const conflicts = checkConflicts(task);
|
||||
|
||||
if (conflicts.hasConflict) {
|
||||
const resolution = options.onConflict?.(task, conflicts) ?? conflicts.resolution ?? "wait";
|
||||
const resolution =
|
||||
options.onConflict?.(task, conflicts) ?? conflicts.resolution ?? "wait";
|
||||
|
||||
const handled = await handleConflict(task, conflicts, resolution, options);
|
||||
const handled = await handleConflict(
|
||||
task,
|
||||
conflicts,
|
||||
resolution,
|
||||
options,
|
||||
);
|
||||
if (!handled.continue) {
|
||||
releaseResources(task, 0, false);
|
||||
return handled.result;
|
||||
@@ -244,7 +263,10 @@ const handleConflict = async <TInput, TOutput>(
|
||||
resolution: ConflictResolution,
|
||||
_options: ParallelExecutorOptions,
|
||||
): Promise<{ continue: boolean; result: ParallelExecutionResult<TOutput> }> => {
|
||||
const createFailResult = (status: "conflict" | "cancelled", error: string) => ({
|
||||
const createFailResult = (
|
||||
status: "conflict" | "cancelled",
|
||||
error: string,
|
||||
) => ({
|
||||
continue: false,
|
||||
result: {
|
||||
taskId: task.id,
|
||||
@@ -258,14 +280,25 @@ const handleConflict = async <TInput, TOutput>(
|
||||
|
||||
const resolutionHandlers: Record<
|
||||
ConflictResolution,
|
||||
() => Promise<{ continue: boolean; result: ParallelExecutionResult<TOutput> }>
|
||||
() => Promise<{
|
||||
continue: boolean;
|
||||
result: ParallelExecutionResult<TOutput>;
|
||||
}>
|
||||
> = {
|
||||
wait: async () => {
|
||||
const resolved = await waitForConflictResolution(conflicts.conflictingTaskIds);
|
||||
const resolved = await waitForConflictResolution(
|
||||
conflicts.conflictingTaskIds,
|
||||
);
|
||||
if (resolved) {
|
||||
return { continue: true, result: {} as ParallelExecutionResult<TOutput> };
|
||||
return {
|
||||
continue: true,
|
||||
result: {} as ParallelExecutionResult<TOutput>,
|
||||
};
|
||||
}
|
||||
return createFailResult("conflict", PARALLEL_ERRORS.CONFLICT(task.id, conflicts.conflictingPaths));
|
||||
return createFailResult(
|
||||
"conflict",
|
||||
PARALLEL_ERRORS.CONFLICT(task.id, conflicts.conflictingPaths),
|
||||
);
|
||||
},
|
||||
|
||||
cancel: async () => {
|
||||
@@ -282,7 +315,10 @@ const handleConflict = async <TInput, TOutput>(
|
||||
},
|
||||
|
||||
abort: async () => {
|
||||
return createFailResult("conflict", PARALLEL_ERRORS.CONFLICT(task.id, conflicts.conflictingPaths));
|
||||
return createFailResult(
|
||||
"conflict",
|
||||
PARALLEL_ERRORS.CONFLICT(task.id, conflicts.conflictingPaths),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -160,7 +160,11 @@ export const acquireResources = async (task: ParallelTask): Promise<void> => {
|
||||
/**
|
||||
* Release resources after task completion
|
||||
*/
|
||||
export const releaseResources = (task: ParallelTask, duration: number, success: boolean): void => {
|
||||
export const releaseResources = (
|
||||
task: ParallelTask,
|
||||
duration: number,
|
||||
success: boolean,
|
||||
): void => {
|
||||
if (!globalSemaphore) return;
|
||||
|
||||
// Release global permit
|
||||
|
||||
@@ -24,7 +24,9 @@ export const collectResults = <TOutput>(
|
||||
results: ParallelExecutionResult<TOutput>[],
|
||||
): AggregatedResults<TOutput> => {
|
||||
const successful = results.filter((r) => r.status === "completed").length;
|
||||
const failed = results.filter((r) => r.status === "error" || r.status === "timeout").length;
|
||||
const failed = results.filter(
|
||||
(r) => r.status === "error" || r.status === "timeout",
|
||||
).length;
|
||||
const cancelled = results.filter((r) => r.status === "cancelled").length;
|
||||
|
||||
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
||||
@@ -97,7 +99,9 @@ export const deduplicateFileResults = (
|
||||
/**
|
||||
* Deduplicate search results (by path and content)
|
||||
*/
|
||||
export const deduplicateSearchResults = <T extends { path: string; match?: string }>(
|
||||
export const deduplicateSearchResults = <
|
||||
T extends { path: string; match?: string },
|
||||
>(
|
||||
results: T[],
|
||||
): DeduplicationResult<T> => {
|
||||
return deduplicateResults(results, (item) => ({
|
||||
@@ -137,9 +141,13 @@ export const mergeByPriority = <T>(
|
||||
const sorted = [...results].sort((a, b) => a.completedAt - b.completedAt);
|
||||
|
||||
// Return the most recent successful result
|
||||
const successful = sorted.filter((r) => r.status === "completed" && r.result !== undefined);
|
||||
const successful = sorted.filter(
|
||||
(r) => r.status === "completed" && r.result !== undefined,
|
||||
);
|
||||
|
||||
return successful.length > 0 ? successful[successful.length - 1].result : undefined;
|
||||
return successful.length > 0
|
||||
? successful[successful.length - 1].result
|
||||
: undefined;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -274,7 +282,5 @@ export const aggregateAll = (
|
||||
export const aggregateAny = (
|
||||
results: ParallelExecutionResult<boolean>[],
|
||||
): boolean => {
|
||||
return results.some(
|
||||
(r) => r.status === "completed" && r.result === true,
|
||||
);
|
||||
return results.some((r) => r.status === "completed" && r.result === true);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { chat as providerChat } from "@providers/index";
|
||||
import { chat as providerChat } from "@providers/core/chat";
|
||||
import type { Message, ProviderName } from "@/types/providers";
|
||||
import type {
|
||||
Plan,
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
PlanStepStatus,
|
||||
PlanStepType,
|
||||
} from "@/types/planner";
|
||||
import { PLAN_SYSTEM_PROMPT } from "@prompts/index";
|
||||
import { PLAN_SYSTEM_PROMPT } from "@prompts/system/planner";
|
||||
|
||||
/**
|
||||
* Status icon mapping
|
||||
|
||||
@@ -28,7 +28,7 @@ import { DIRS, LOCAL_CONFIG_DIR } from "@constants/paths";
|
||||
* Discover plugins in a directory
|
||||
*/
|
||||
const discoverPluginsInDir = async (
|
||||
baseDir: string
|
||||
baseDir: string,
|
||||
): Promise<PluginDiscoveryResult[]> => {
|
||||
const pluginsPath = join(baseDir, PLUGINS_DIR);
|
||||
const results: PluginDiscoveryResult[] = [];
|
||||
@@ -66,7 +66,7 @@ const discoverPluginsInDir = async (
|
||||
* Discover all plugins from global and local directories
|
||||
*/
|
||||
export const discoverPlugins = async (
|
||||
workingDir: string
|
||||
workingDir: string,
|
||||
): Promise<PluginDiscoveryResult[]> => {
|
||||
const [globalPlugins, localPlugins] = await Promise.all([
|
||||
discoverPluginsInDir(DIRS.config),
|
||||
@@ -91,7 +91,7 @@ export const discoverPlugins = async (
|
||||
* Parse plugin manifest
|
||||
*/
|
||||
export const parseManifest = async (
|
||||
manifestPath: string
|
||||
manifestPath: string,
|
||||
): Promise<PluginManifest | null> => {
|
||||
try {
|
||||
const content = await readFile(manifestPath, "utf-8");
|
||||
@@ -112,7 +112,7 @@ export const parseManifest = async (
|
||||
* Parse command file with frontmatter
|
||||
*/
|
||||
export const parseCommandFile = async (
|
||||
filePath: string
|
||||
filePath: string,
|
||||
): Promise<PluginCommandDefinition | null> => {
|
||||
try {
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
@@ -157,7 +157,10 @@ export const parseCommandFile = async (
|
||||
}
|
||||
|
||||
// Rest is the prompt
|
||||
const prompt = lines.slice(endIndex + 1).join("\n").trim();
|
||||
const prompt = lines
|
||||
.slice(endIndex + 1)
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
const name = frontmatter.name || basename(filePath, COMMAND_FILE_EXTENSION);
|
||||
const description = frontmatter.description || `Custom command: ${name}`;
|
||||
@@ -176,7 +179,7 @@ export const parseCommandFile = async (
|
||||
* Load tool module dynamically
|
||||
*/
|
||||
export const loadToolModule = async (
|
||||
filePath: string
|
||||
filePath: string,
|
||||
): Promise<PluginToolDefinition | null> => {
|
||||
try {
|
||||
// For Bun, we can use dynamic import
|
||||
@@ -184,7 +187,12 @@ export const loadToolModule = async (
|
||||
const toolDef = module.default || module;
|
||||
|
||||
// Validate tool definition
|
||||
if (!toolDef.name || !toolDef.description || !toolDef.parameters || !toolDef.execute) {
|
||||
if (
|
||||
!toolDef.name ||
|
||||
!toolDef.description ||
|
||||
!toolDef.parameters ||
|
||||
!toolDef.execute
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -199,7 +207,7 @@ export const loadToolModule = async (
|
||||
*/
|
||||
export const loadPluginHooks = async (
|
||||
pluginPath: string,
|
||||
manifest: PluginManifest
|
||||
manifest: PluginManifest,
|
||||
): Promise<HookDefinition[]> => {
|
||||
const hooks: HookDefinition[] = [];
|
||||
|
||||
@@ -253,7 +261,7 @@ export const loadPluginHooks = async (
|
||||
if (baseName.toLowerCase().includes(eventType.toLowerCase())) {
|
||||
// Check if already added from manifest
|
||||
const alreadyAdded = hooks.some(
|
||||
(h) => h.script === scriptPath && h.event === eventType
|
||||
(h) => h.script === scriptPath && h.event === eventType,
|
||||
);
|
||||
|
||||
if (!alreadyAdded) {
|
||||
@@ -279,7 +287,7 @@ export const loadPluginHooks = async (
|
||||
*/
|
||||
export const loadPluginCommands = async (
|
||||
pluginPath: string,
|
||||
manifest: PluginManifest
|
||||
manifest: PluginManifest,
|
||||
): Promise<Map<string, PluginCommandDefinition>> => {
|
||||
const commands = new Map<string, PluginCommandDefinition>();
|
||||
|
||||
@@ -329,7 +337,7 @@ export const loadPluginCommands = async (
|
||||
*/
|
||||
export const loadPluginTools = async (
|
||||
pluginPath: string,
|
||||
manifest: PluginManifest
|
||||
manifest: PluginManifest,
|
||||
): Promise<Map<string, PluginToolDefinition>> => {
|
||||
const tools = new Map<string, PluginToolDefinition>();
|
||||
|
||||
|
||||
@@ -19,10 +19,7 @@ import {
|
||||
loadPluginCommands,
|
||||
loadPluginHooks,
|
||||
} from "@services/plugin-loader";
|
||||
import {
|
||||
PLUGIN_TOOL_SEPARATOR,
|
||||
PLUGIN_ERRORS,
|
||||
} from "@constants/plugin";
|
||||
import { PLUGIN_TOOL_SEPARATOR, PLUGIN_ERRORS } from "@constants/plugin";
|
||||
|
||||
/**
|
||||
* Plugin registry singleton
|
||||
@@ -40,7 +37,7 @@ const registry: PluginRegistry = {
|
||||
const loadPlugin = async (
|
||||
_name: string,
|
||||
path: string,
|
||||
manifestPath: string
|
||||
manifestPath: string,
|
||||
): Promise<PluginLoadResult> => {
|
||||
const manifest = await parseManifest(manifestPath);
|
||||
|
||||
@@ -86,7 +83,7 @@ export const initializePlugins = async (workingDir: string): Promise<void> => {
|
||||
const result = await loadPlugin(
|
||||
discovered.name,
|
||||
discovered.path,
|
||||
discovered.manifestPath
|
||||
discovered.manifestPath,
|
||||
);
|
||||
|
||||
if (result.success && result.plugin) {
|
||||
@@ -173,7 +170,7 @@ export const getPluginToolsForApi = (): {
|
||||
* Get a plugin command by name
|
||||
*/
|
||||
export const getPluginCommand = (
|
||||
name: string
|
||||
name: string,
|
||||
): PluginCommandDefinition | undefined => {
|
||||
return registry.commands.get(name);
|
||||
};
|
||||
|
||||
@@ -185,7 +185,10 @@ export const parseDiff = (diffContent: string): ParsedDiff => {
|
||||
/**
|
||||
* Create empty file diff structure
|
||||
*/
|
||||
const createEmptyFileDiff = (oldPath: string, newPath: string): ParsedFileDiff => ({
|
||||
const createEmptyFileDiff = (
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
): ParsedFileDiff => ({
|
||||
oldPath: cleanPath(oldPath),
|
||||
newPath: cleanPath(newPath),
|
||||
hunks: [],
|
||||
|
||||
@@ -9,8 +9,15 @@ import {
|
||||
PR_REVIEW_ERRORS,
|
||||
PR_REVIEW_MESSAGES,
|
||||
} from "@constants/pr-review";
|
||||
import { parseDiff, filterFiles, getFilePath } from "@services/pr-review/diff-parser";
|
||||
import { generateReport, formatReportMarkdown } from "@services/pr-review/report-generator";
|
||||
import {
|
||||
parseDiff,
|
||||
filterFiles,
|
||||
getFilePath,
|
||||
} from "@services/pr-review/diff-parser";
|
||||
import {
|
||||
generateReport,
|
||||
formatReportMarkdown,
|
||||
} from "@services/pr-review/report-generator";
|
||||
import * as securityReviewer from "@services/pr-review/reviewers/security";
|
||||
import * as performanceReviewer from "@services/pr-review/reviewers/performance";
|
||||
import * as logicReviewer from "@services/pr-review/reviewers/logic";
|
||||
@@ -120,7 +127,8 @@ const runReviewers = async (
|
||||
onProgress?.(PR_REVIEW_MESSAGES.REVIEWING(reviewerConfig.name));
|
||||
|
||||
const startTime = Date.now();
|
||||
const reviewerModule = reviewers[reviewerConfig.name as keyof typeof reviewers];
|
||||
const reviewerModule =
|
||||
reviewers[reviewerConfig.name as keyof typeof reviewers];
|
||||
|
||||
if (!reviewerModule) {
|
||||
return {
|
||||
@@ -180,7 +188,12 @@ export const quickReview = async (
|
||||
{
|
||||
config: {
|
||||
reviewers: [
|
||||
{ name: "security", type: "security", enabled: true, minConfidence: 90 },
|
||||
{
|
||||
name: "security",
|
||||
type: "security",
|
||||
enabled: true,
|
||||
minConfidence: 90,
|
||||
},
|
||||
{ name: "logic", type: "logic", enabled: true, minConfidence: 90 },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -235,7 +235,8 @@ const calculateRecommendation = (
|
||||
|
||||
if (
|
||||
bySeverity.critical === 0 &&
|
||||
bySeverity.warning <= RECOMMENDATION_THRESHOLDS.approve_with_suggestions.maxWarning
|
||||
bySeverity.warning <=
|
||||
RECOMMENDATION_THRESHOLDS.approve_with_suggestions.maxWarning
|
||||
) {
|
||||
return "approve_with_suggestions";
|
||||
}
|
||||
@@ -264,7 +265,9 @@ const generateSummary = (
|
||||
// Count by severity
|
||||
const critical = findings.filter((f) => f.severity === "critical").length;
|
||||
const warnings = findings.filter((f) => f.severity === "warning").length;
|
||||
const suggestions = findings.filter((f) => f.severity === "suggestion").length;
|
||||
const suggestions = findings.filter(
|
||||
(f) => f.severity === "suggestion",
|
||||
).length;
|
||||
|
||||
if (critical > 0) {
|
||||
parts.push(`${critical} critical issue(s) must be addressed`);
|
||||
@@ -318,7 +321,12 @@ export const formatReportMarkdown = (report: PRReviewReport): string => {
|
||||
// Findings by severity
|
||||
lines.push("| Severity | Count |");
|
||||
lines.push("|----------|-------|");
|
||||
for (const severity of ["critical", "warning", "suggestion", "nitpick"] as const) {
|
||||
for (const severity of [
|
||||
"critical",
|
||||
"warning",
|
||||
"suggestion",
|
||||
"nitpick",
|
||||
] as const) {
|
||||
const count = report.findingsBySeverity[severity];
|
||||
if (count > 0) {
|
||||
lines.push(
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
* Analyzes code for logical errors and edge cases.
|
||||
*/
|
||||
|
||||
import { MIN_CONFIDENCE_THRESHOLD, REVIEWER_PROMPTS } from "@constants/pr-review";
|
||||
import {
|
||||
MIN_CONFIDENCE_THRESHOLD,
|
||||
REVIEWER_PROMPTS,
|
||||
} from "@constants/pr-review";
|
||||
import type {
|
||||
PRReviewFinding,
|
||||
ParsedFileDiff,
|
||||
@@ -17,8 +20,8 @@ import type {
|
||||
const LOGIC_PATTERNS = {
|
||||
MISSING_NULL_CHECK: {
|
||||
patterns: [
|
||||
/\w+\.\w+\.\w+/, // Deep property access without optional chaining
|
||||
/(\w+)\[['"][^'"]+['"]\]\.\w+/, // Object property followed by method
|
||||
/\w+\.\w+\.\w+/, // Deep property access without optional chaining
|
||||
/(\w+)\[['"][^'"]+['"]\]\.\w+/, // Object property followed by method
|
||||
],
|
||||
message: "Potential null/undefined reference",
|
||||
suggestion: "Use optional chaining (?.) or add null checks",
|
||||
@@ -27,7 +30,7 @@ const LOGIC_PATTERNS = {
|
||||
|
||||
OPTIONAL_CHAIN_MISSING: {
|
||||
patterns: [
|
||||
/if\s*\([^)]*\)\s*\{[^}]*\w+\./, // Variable used after if check without ?.
|
||||
/if\s*\([^)]*\)\s*\{[^}]*\w+\./, // Variable used after if check without ?.
|
||||
],
|
||||
message: "Consider using optional chaining",
|
||||
suggestion: "Replace conditional access with ?. operator",
|
||||
@@ -35,10 +38,7 @@ const LOGIC_PATTERNS = {
|
||||
},
|
||||
|
||||
EMPTY_CATCH: {
|
||||
patterns: [
|
||||
/catch\s*\([^)]*\)\s*\{\s*\}/,
|
||||
/catch\s*\{\s*\}/,
|
||||
],
|
||||
patterns: [/catch\s*\([^)]*\)\s*\{\s*\}/, /catch\s*\{\s*\}/],
|
||||
message: "Empty catch block - errors silently ignored",
|
||||
suggestion: "Log the error or handle it appropriately",
|
||||
confidence: 90,
|
||||
@@ -54,40 +54,28 @@ const LOGIC_PATTERNS = {
|
||||
},
|
||||
|
||||
FLOATING_PROMISE: {
|
||||
patterns: [
|
||||
/^\s*\w+\s*\.\s*then\s*\(/m,
|
||||
/^\s*\w+\([^)]*\)\.then\s*\(/m,
|
||||
],
|
||||
patterns: [/^\s*\w+\s*\.\s*then\s*\(/m, /^\s*\w+\([^)]*\)\.then\s*\(/m],
|
||||
message: "Floating promise - missing await or error handling",
|
||||
suggestion: "Use await or add .catch() for error handling",
|
||||
confidence: 80,
|
||||
},
|
||||
|
||||
ARRAY_INDEX_ACCESS: {
|
||||
patterns: [
|
||||
/\[\d+\]/,
|
||||
/\[0\]/,
|
||||
/\[-1\]/,
|
||||
],
|
||||
patterns: [/\[\d+\]/, /\[0\]/, /\[-1\]/],
|
||||
message: "Direct array index access without bounds check",
|
||||
suggestion: "Consider using .at() or add bounds checking",
|
||||
confidence: 60,
|
||||
},
|
||||
|
||||
EQUALITY_TYPE_COERCION: {
|
||||
patterns: [
|
||||
/[^=!]==[^=]/,
|
||||
/[^!]!=[^=]/,
|
||||
],
|
||||
patterns: [/[^=!]==[^=]/, /[^!]!=[^=]/],
|
||||
message: "Using == instead of === (type coercion)",
|
||||
suggestion: "Use strict equality (===) to avoid type coercion bugs",
|
||||
confidence: 85,
|
||||
},
|
||||
|
||||
ASYNC_IN_FOREACH: {
|
||||
patterns: [
|
||||
/\.forEach\s*\(\s*async/,
|
||||
],
|
||||
patterns: [/\.forEach\s*\(\s*async/],
|
||||
message: "Async callback in forEach - won't await properly",
|
||||
suggestion: "Use for...of loop or Promise.all with .map()",
|
||||
confidence: 90,
|
||||
@@ -104,19 +92,14 @@ const LOGIC_PATTERNS = {
|
||||
},
|
||||
|
||||
RACE_CONDITION: {
|
||||
patterns: [
|
||||
/let\s+\w+\s*=[^;]+;\s*await\s+[^;]+;\s*\w+\s*=/,
|
||||
],
|
||||
patterns: [/let\s+\w+\s*=[^;]+;\s*await\s+[^;]+;\s*\w+\s*=/],
|
||||
message: "Potential race condition with shared state",
|
||||
suggestion: "Use atomic operations or proper synchronization",
|
||||
confidence: 70,
|
||||
},
|
||||
|
||||
INFINITE_LOOP_RISK: {
|
||||
patterns: [
|
||||
/while\s*\(\s*true\s*\)/,
|
||||
/for\s*\(\s*;\s*;\s*\)/,
|
||||
],
|
||||
patterns: [/while\s*\(\s*true\s*\)/, /for\s*\(\s*;\s*;\s*\)/],
|
||||
message: "Infinite loop without clear exit condition",
|
||||
suggestion: "Ensure there's a clear break condition",
|
||||
confidence: 75,
|
||||
@@ -204,7 +187,9 @@ const getAllAddedLines = (
|
||||
/**
|
||||
* Deduplicate findings with same message on adjacent lines
|
||||
*/
|
||||
const deduplicateFindings = (findings: PRReviewFinding[]): PRReviewFinding[] => {
|
||||
const deduplicateFindings = (
|
||||
findings: PRReviewFinding[],
|
||||
): PRReviewFinding[] => {
|
||||
const seen = new Map<string, PRReviewFinding>();
|
||||
|
||||
for (const finding of findings) {
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
* Analyzes code for performance issues.
|
||||
*/
|
||||
|
||||
import { MIN_CONFIDENCE_THRESHOLD, REVIEWER_PROMPTS } from "@constants/pr-review";
|
||||
import {
|
||||
MIN_CONFIDENCE_THRESHOLD,
|
||||
REVIEWER_PROMPTS,
|
||||
} from "@constants/pr-review";
|
||||
import type {
|
||||
PRReviewFinding,
|
||||
ParsedFileDiff,
|
||||
@@ -23,7 +26,8 @@ const PERFORMANCE_PATTERNS = {
|
||||
/while\s*\([^)]+\)\s*\{[^}]*while\s*\([^)]+\)/,
|
||||
],
|
||||
message: "Nested loops detected - potential O(n²) complexity",
|
||||
suggestion: "Consider using a Map/Set for O(1) lookups or restructuring the algorithm",
|
||||
suggestion:
|
||||
"Consider using a Map/Set for O(1) lookups or restructuring the algorithm",
|
||||
confidence: 75,
|
||||
},
|
||||
|
||||
@@ -47,7 +51,8 @@ const PERFORMANCE_PATTERNS = {
|
||||
/style\s*=\s*\{\s*\{/,
|
||||
],
|
||||
message: "Potential unnecessary re-render in React component",
|
||||
suggestion: "Use useMemo/useCallback for objects/arrays, extract styles outside component",
|
||||
suggestion:
|
||||
"Use useMemo/useCallback for objects/arrays, extract styles outside component",
|
||||
confidence: 70,
|
||||
},
|
||||
|
||||
@@ -90,7 +95,8 @@ const PERFORMANCE_PATTERNS = {
|
||||
/require\s*\(\s*['"]lodash['"]\s*\)/,
|
||||
],
|
||||
message: "Large library import may increase bundle size",
|
||||
suggestion: "Use specific imports (lodash/get) or smaller alternatives (date-fns)",
|
||||
suggestion:
|
||||
"Use specific imports (lodash/get) or smaller alternatives (date-fns)",
|
||||
confidence: 80,
|
||||
},
|
||||
|
||||
@@ -102,7 +108,8 @@ const PERFORMANCE_PATTERNS = {
|
||||
/existsSync\s*\(/,
|
||||
],
|
||||
message: "Synchronous file operation may block event loop",
|
||||
suggestion: "Use async versions (readFile, writeFile) for better performance",
|
||||
suggestion:
|
||||
"Use async versions (readFile, writeFile) for better performance",
|
||||
confidence: 80,
|
||||
},
|
||||
} as const;
|
||||
@@ -120,7 +127,7 @@ export const reviewFile = (
|
||||
const addedLines = getAllAddedLines(diff);
|
||||
|
||||
// Combine lines for multi-line pattern matching
|
||||
const combinedContent = addedLines.map(l => l.content).join("\n");
|
||||
const combinedContent = addedLines.map((l) => l.content).join("\n");
|
||||
|
||||
// Check each pattern
|
||||
for (const [patternName, config] of Object.entries(PERFORMANCE_PATTERNS)) {
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
* Analyzes code for security vulnerabilities.
|
||||
*/
|
||||
|
||||
import { MIN_CONFIDENCE_THRESHOLD, REVIEWER_PROMPTS } from "@constants/pr-review";
|
||||
import {
|
||||
MIN_CONFIDENCE_THRESHOLD,
|
||||
REVIEWER_PROMPTS,
|
||||
} from "@constants/pr-review";
|
||||
import type {
|
||||
PRReviewFinding,
|
||||
ParsedFileDiff,
|
||||
@@ -51,7 +54,8 @@ const SECURITY_PATTERNS = {
|
||||
/\$\(.* \+ /,
|
||||
],
|
||||
message: "Potential command injection vulnerability",
|
||||
suggestion: "Avoid string concatenation in shell commands, use argument arrays",
|
||||
suggestion:
|
||||
"Avoid string concatenation in shell commands, use argument arrays",
|
||||
confidence: 90,
|
||||
},
|
||||
|
||||
@@ -82,11 +86,10 @@ const SECURITY_PATTERNS = {
|
||||
},
|
||||
|
||||
INSECURE_RANDOM: {
|
||||
patterns: [
|
||||
/Math\.random\s*\(\)/,
|
||||
],
|
||||
patterns: [/Math\.random\s*\(\)/],
|
||||
message: "Insecure random number generation",
|
||||
suggestion: "Use crypto.randomBytes or crypto.getRandomValues for security-sensitive operations",
|
||||
suggestion:
|
||||
"Use crypto.randomBytes or crypto.getRandomValues for security-sensitive operations",
|
||||
confidence: 70,
|
||||
},
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
* Analyzes code for style and consistency issues.
|
||||
*/
|
||||
|
||||
import { MIN_CONFIDENCE_THRESHOLD, REVIEWER_PROMPTS } from "@constants/pr-review";
|
||||
import {
|
||||
MIN_CONFIDENCE_THRESHOLD,
|
||||
REVIEWER_PROMPTS,
|
||||
} from "@constants/pr-review";
|
||||
import type {
|
||||
PRReviewFinding,
|
||||
ParsedFileDiff,
|
||||
@@ -16,9 +19,7 @@ import type {
|
||||
*/
|
||||
const STYLE_PATTERNS = {
|
||||
CONSOLE_LOG: {
|
||||
patterns: [
|
||||
/console\.(log|debug|info)\s*\(/,
|
||||
],
|
||||
patterns: [/console\.(log|debug|info)\s*\(/],
|
||||
message: "Console statement left in code",
|
||||
suggestion: "Remove console statements before committing or use a logger",
|
||||
confidence: 85,
|
||||
@@ -46,36 +47,28 @@ const STYLE_PATTERNS = {
|
||||
},
|
||||
|
||||
LONG_LINE: {
|
||||
patterns: [
|
||||
/.{121,}/,
|
||||
],
|
||||
patterns: [/.{121,}/],
|
||||
message: "Line exceeds 120 characters",
|
||||
suggestion: "Break long lines for better readability",
|
||||
confidence: 75,
|
||||
},
|
||||
|
||||
INCONSISTENT_QUOTES: {
|
||||
patterns: [
|
||||
/["'][^"']*["']/,
|
||||
],
|
||||
patterns: [/["'][^"']*["']/],
|
||||
message: "Inconsistent quote style",
|
||||
suggestion: "Use consistent quotes (single or double) throughout the file",
|
||||
confidence: 60,
|
||||
},
|
||||
|
||||
VAR_DECLARATION: {
|
||||
patterns: [
|
||||
/\bvar\s+\w+/,
|
||||
],
|
||||
patterns: [/\bvar\s+\w+/],
|
||||
message: "Using 'var' instead of 'let' or 'const'",
|
||||
suggestion: "Prefer 'const' for immutable values, 'let' for mutable",
|
||||
confidence: 85,
|
||||
},
|
||||
|
||||
NESTED_TERNARY: {
|
||||
patterns: [
|
||||
/\?[^:]+\?[^:]+:/,
|
||||
],
|
||||
patterns: [/\?[^:]+\?[^:]+:/],
|
||||
message: "Nested ternary operator - hard to read",
|
||||
suggestion: "Use if-else statements or extract to a function",
|
||||
confidence: 80,
|
||||
@@ -92,20 +85,14 @@ const STYLE_PATTERNS = {
|
||||
},
|
||||
|
||||
ANY_TYPE: {
|
||||
patterns: [
|
||||
/:\s*any\b/,
|
||||
/<any>/,
|
||||
/as\s+any\b/,
|
||||
],
|
||||
patterns: [/:\s*any\b/, /<any>/, /as\s+any\b/],
|
||||
message: "Using 'any' type reduces type safety",
|
||||
suggestion: "Use specific types or 'unknown' with type guards",
|
||||
confidence: 75,
|
||||
},
|
||||
|
||||
SINGLE_LETTER_VAR: {
|
||||
patterns: [
|
||||
/\b(?:const|let|var)\s+[a-z]\s*=/,
|
||||
],
|
||||
patterns: [/\b(?:const|let|var)\s+[a-z]\s*=/],
|
||||
message: "Single-letter variable name",
|
||||
suggestion: "Use descriptive variable names for clarity",
|
||||
confidence: 65,
|
||||
@@ -122,9 +109,7 @@ const STYLE_PATTERNS = {
|
||||
},
|
||||
|
||||
DUPLICATE_IMPORT: {
|
||||
patterns: [
|
||||
/import\s+\{[^}]+\}\s+from\s+['"]([^'"]+)['"]/,
|
||||
],
|
||||
patterns: [/import\s+\{[^}]+\}\s+from\s+['"]([^'"]+)['"]/],
|
||||
message: "Check for duplicate or unused imports",
|
||||
suggestion: "Consolidate imports from the same module",
|
||||
confidence: 60,
|
||||
@@ -158,7 +143,9 @@ export const reviewFile = (
|
||||
// For some patterns, only report once per file
|
||||
if (shouldReportOncePerFile(patternName)) {
|
||||
if (!foundInFile) {
|
||||
findings.push(createFinding(path, lineNumber, config, patternName));
|
||||
findings.push(
|
||||
createFinding(path, lineNumber, config, patternName),
|
||||
);
|
||||
foundInFile = true;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -58,9 +58,10 @@ const addToGitignore = async (workingDir: string): Promise<boolean> => {
|
||||
}
|
||||
|
||||
// Add .codetyper to gitignore
|
||||
const newContent = content.endsWith("\n") || content === ""
|
||||
? `${content}${GITIGNORE_ENTRY}\n`
|
||||
: `${content}\n${GITIGNORE_ENTRY}\n`;
|
||||
const newContent =
|
||||
content.endsWith("\n") || content === ""
|
||||
? `${content}${GITIGNORE_ENTRY}\n`
|
||||
: `${content}\n${GITIGNORE_ENTRY}\n`;
|
||||
|
||||
await fs.writeFile(gitignorePath, newContent, "utf-8");
|
||||
return true;
|
||||
@@ -340,7 +341,9 @@ const createDefaultAgents = async (workingDir: string): Promise<string[]> => {
|
||||
return created;
|
||||
};
|
||||
|
||||
export const setupProject = async (workingDir: string): Promise<SetupResult> => {
|
||||
export const setupProject = async (
|
||||
workingDir: string,
|
||||
): Promise<SetupResult> => {
|
||||
const result: SetupResult = {
|
||||
gitignoreUpdated: false,
|
||||
agentsCreated: [],
|
||||
@@ -365,13 +368,17 @@ export const setupProject = async (workingDir: string): Promise<SetupResult> =>
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getSetupStatus = async (workingDir: string): Promise<{
|
||||
export const getSetupStatus = async (
|
||||
workingDir: string,
|
||||
): Promise<{
|
||||
hasGit: boolean;
|
||||
hasCodetyperDir: boolean;
|
||||
agentCount: number;
|
||||
}> => {
|
||||
const hasGit = await isGitRepository(workingDir);
|
||||
const hasCodetyperDir = await fileExists(path.join(workingDir, CODETYPER_DIR));
|
||||
const hasCodetyperDir = await fileExists(
|
||||
path.join(workingDir, CODETYPER_DIR),
|
||||
);
|
||||
|
||||
let agentCount = 0;
|
||||
if (hasCodetyperDir) {
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
|
||||
import { join } from "path";
|
||||
import { homedir } from "os";
|
||||
import type { ProviderQualityData, TaskType, QualityScore } from "@/types/provider-quality";
|
||||
import type {
|
||||
ProviderQualityData,
|
||||
TaskType,
|
||||
QualityScore,
|
||||
} from "@/types/provider-quality";
|
||||
import { QUALITY_THRESHOLDS } from "@constants/provider-quality";
|
||||
|
||||
const QUALITY_DATA_DIR = join(homedir(), ".config", "codetyper");
|
||||
|
||||
@@ -18,7 +18,8 @@ export interface RoutingContext {
|
||||
export const determineRoute = async (
|
||||
context: RoutingContext,
|
||||
): Promise<RoutingDecision> => {
|
||||
const { taskType, ollamaAvailable, copilotAvailable, cascadeEnabled } = context;
|
||||
const { taskType, ollamaAvailable, copilotAvailable, cascadeEnabled } =
|
||||
context;
|
||||
|
||||
if (!ollamaAvailable && !copilotAvailable) {
|
||||
throw new Error("No providers available");
|
||||
|
||||
@@ -11,7 +11,12 @@ import {
|
||||
calculateOverallScore,
|
||||
} from "./persistence";
|
||||
|
||||
export type Outcome = "approved" | "corrected" | "rejected" | "minor_issue" | "major_issue";
|
||||
export type Outcome =
|
||||
| "approved"
|
||||
| "corrected"
|
||||
| "rejected"
|
||||
| "minor_issue"
|
||||
| "major_issue";
|
||||
|
||||
export interface ScoreUpdate {
|
||||
providerId: string;
|
||||
@@ -19,7 +24,9 @@ export interface ScoreUpdate {
|
||||
outcome: Outcome;
|
||||
}
|
||||
|
||||
export const updateQualityScore = async (update: ScoreUpdate): Promise<void> => {
|
||||
export const updateQualityScore = async (
|
||||
update: ScoreUpdate,
|
||||
): Promise<void> => {
|
||||
const { providerId, taskType, outcome } = update;
|
||||
const data = await getProviderQuality(providerId);
|
||||
const score = data.scores[taskType];
|
||||
|
||||
@@ -44,7 +44,9 @@ export const getTaskTypeConfidence = (
|
||||
return 0.3;
|
||||
}
|
||||
|
||||
const matchCount = patterns.filter((p) => p.test(prompt.toLowerCase())).length;
|
||||
const matchCount = patterns.filter((p) =>
|
||||
p.test(prompt.toLowerCase()),
|
||||
).length;
|
||||
const confidence = Math.min(0.5 + matchCount * 0.15, 1.0);
|
||||
|
||||
return confidence;
|
||||
|
||||
@@ -21,7 +21,7 @@ import type {
|
||||
ToolCallMessage,
|
||||
ToolResultMessage,
|
||||
} from "@/types/agent";
|
||||
import { chat as providerChat } from "@providers/index";
|
||||
import { chat as providerChat } from "@providers/core/chat";
|
||||
import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index";
|
||||
import type { ToolContext, ToolCall, ToolResult } from "@/types/tools";
|
||||
import { initializePermissions } from "@services/core/permissions";
|
||||
@@ -37,18 +37,19 @@ import type {
|
||||
|
||||
import {
|
||||
createInitialState,
|
||||
evaluateResponseQuality,
|
||||
decideRetry,
|
||||
checkTermination,
|
||||
createMemoryStore,
|
||||
addMemory,
|
||||
createMemoryItem,
|
||||
evaluateResponseQuality,
|
||||
decideRetry,
|
||||
} from "@services/reasoning/orchestrator";
|
||||
import {
|
||||
compressContext,
|
||||
markMessagesWithAge,
|
||||
getPreservationCandidates,
|
||||
checkTermination,
|
||||
estimateTokens,
|
||||
createTimestamp,
|
||||
} from "@services/reasoning";
|
||||
} from "@services/reasoning/context-compression";
|
||||
import { estimateTokens, createTimestamp } from "@services/reasoning/utils";
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
|
||||
@@ -45,7 +45,10 @@ const COMMAND_INJECTION_PATTERNS = [
|
||||
{ pattern: /\x00/, description: "Null byte detected" },
|
||||
// Environment variable expansion
|
||||
{ pattern: /\$\{[^}]+\}/, description: "Environment variable expansion" },
|
||||
{ pattern: /\$[A-Za-z_][A-Za-z0-9_]*/, description: "Variable reference detected" },
|
||||
{
|
||||
pattern: /\$[A-Za-z_][A-Za-z0-9_]*/,
|
||||
description: "Variable reference detected",
|
||||
},
|
||||
];
|
||||
|
||||
// XSS patterns
|
||||
@@ -57,7 +60,10 @@ const XSS_PATTERNS = [
|
||||
// JavaScript protocol
|
||||
{ pattern: /javascript:/i, description: "JavaScript protocol detected" },
|
||||
// Data URLs with script content
|
||||
{ pattern: /data:[^,]*;base64/i, description: "Data URL with base64 encoding" },
|
||||
{
|
||||
pattern: /data:[^,]*;base64/i,
|
||||
description: "Data URL with base64 encoding",
|
||||
},
|
||||
// Expression/eval
|
||||
{ pattern: /expression\s*\(/i, description: "CSS expression detected" },
|
||||
// SVG with script
|
||||
@@ -84,9 +90,15 @@ const DANGEROUS_CALLS_PATTERNS = [
|
||||
{ pattern: /exec\s*\(/i, description: "exec() usage detected" },
|
||||
{ pattern: /system\s*\(/i, description: "system() call detected" },
|
||||
{ pattern: /os\.system\s*\(/i, description: "os.system() call detected" },
|
||||
{ pattern: /subprocess\.call\s*\(/i, description: "subprocess.call() detected" },
|
||||
{
|
||||
pattern: /subprocess\.call\s*\(/i,
|
||||
description: "subprocess.call() detected",
|
||||
},
|
||||
{ pattern: /child_process/i, description: "child_process module usage" },
|
||||
{ pattern: /pickle\.loads?\s*\(/i, description: "Pickle deserialization detected" },
|
||||
{
|
||||
pattern: /pickle\.loads?\s*\(/i,
|
||||
description: "Pickle deserialization detected",
|
||||
},
|
||||
{ pattern: /yaml\.unsafe_load\s*\(/i, description: "Unsafe YAML loading" },
|
||||
{ pattern: /unserialize\s*\(/i, description: "PHP unserialize() detected" },
|
||||
];
|
||||
@@ -105,18 +117,31 @@ const TOKEN_PATTERNS = [
|
||||
// Generic API keys
|
||||
{ pattern: /api[_-]?key[=:]["']?[a-zA-Z0-9_-]{20,}["']?/i, type: "API Key" },
|
||||
// OAuth tokens
|
||||
{ pattern: /bearer\s+[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/i, type: "JWT Token" },
|
||||
{ pattern: /oauth[_-]?token[=:]["']?[a-zA-Z0-9_-]{20,}["']?/i, type: "OAuth Token" },
|
||||
{
|
||||
pattern: /bearer\s+[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/i,
|
||||
type: "JWT Token",
|
||||
},
|
||||
{
|
||||
pattern: /oauth[_-]?token[=:]["']?[a-zA-Z0-9_-]{20,}["']?/i,
|
||||
type: "OAuth Token",
|
||||
},
|
||||
// AWS credentials
|
||||
{ pattern: /AKIA[0-9A-Z]{16}/i, type: "AWS Access Key" },
|
||||
{ pattern: /aws[_-]?secret[_-]?access[_-]?key[=:]["']?[a-zA-Z0-9/+=]{40}["']?/i, type: "AWS Secret Key" },
|
||||
{
|
||||
pattern:
|
||||
/aws[_-]?secret[_-]?access[_-]?key[=:]["']?[a-zA-Z0-9/+=]{40}["']?/i,
|
||||
type: "AWS Secret Key",
|
||||
},
|
||||
// GitHub tokens
|
||||
{ pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/i, type: "GitHub Token" },
|
||||
// Generic secrets
|
||||
{ pattern: /password[=:]["']?[^\s"']{8,}["']?/i, type: "Password" },
|
||||
{ pattern: /secret[=:]["']?[^\s"']{8,}["']?/i, type: "Secret" },
|
||||
// Private keys
|
||||
{ pattern: /-----BEGIN\s+(?:RSA|DSA|EC|OPENSSH)?\s*PRIVATE\s+KEY-----/i, type: "Private Key" },
|
||||
{
|
||||
pattern: /-----BEGIN\s+(?:RSA|DSA|EC|OPENSSH)?\s*PRIVATE\s+KEY-----/i,
|
||||
type: "Private Key",
|
||||
},
|
||||
];
|
||||
|
||||
const checkPatterns = (
|
||||
@@ -156,11 +181,21 @@ export const detectXSS = (content: string): SecurityIssue[] => {
|
||||
};
|
||||
|
||||
export const detectSQLInjection = (content: string): SecurityIssue[] => {
|
||||
return checkPatterns(content, SQL_INJECTION_PATTERNS, "sql_injection", "critical");
|
||||
return checkPatterns(
|
||||
content,
|
||||
SQL_INJECTION_PATTERNS,
|
||||
"sql_injection",
|
||||
"critical",
|
||||
);
|
||||
};
|
||||
|
||||
export const detectDangerousCalls = (code: string): SecurityIssue[] => {
|
||||
return checkPatterns(code, DANGEROUS_CALLS_PATTERNS, "dangerous_call", "high");
|
||||
return checkPatterns(
|
||||
code,
|
||||
DANGEROUS_CALLS_PATTERNS,
|
||||
"dangerous_call",
|
||||
"high",
|
||||
);
|
||||
};
|
||||
|
||||
export const detectShellContinuation = (command: string): SecurityIssue[] => {
|
||||
@@ -200,7 +235,9 @@ export const filterSensitiveTokens = (content: string): string => {
|
||||
for (const { pattern } of TOKEN_PATTERNS) {
|
||||
filtered = filtered.replace(new RegExp(pattern, "gi"), (match) => {
|
||||
if (match.length > 12) {
|
||||
return match.slice(0, 4) + "*".repeat(match.length - 8) + match.slice(-4);
|
||||
return (
|
||||
match.slice(0, 4) + "*".repeat(match.length - 8) + match.slice(-4)
|
||||
);
|
||||
}
|
||||
return "*".repeat(match.length);
|
||||
});
|
||||
@@ -262,7 +299,9 @@ export const explainPermission = (
|
||||
|
||||
return {
|
||||
explanation: `Execute shell command: ${command.slice(0, 100)}${command.length > 100 ? "..." : ""}`,
|
||||
risks: report.issues.map((i) => `${i.risk.toUpperCase()}: ${i.description}`),
|
||||
risks: report.issues.map(
|
||||
(i) => `${i.risk.toUpperCase()}: ${i.description}`,
|
||||
),
|
||||
recommendation: report.hasCritical
|
||||
? "DENY - Critical security risk detected"
|
||||
: report.hasHigh
|
||||
@@ -272,7 +311,8 @@ export const explainPermission = (
|
||||
},
|
||||
|
||||
write: (args) => {
|
||||
const filePath = (args.path as string) ?? (args.file_path as string) ?? "";
|
||||
const filePath =
|
||||
(args.path as string) ?? (args.file_path as string) ?? "";
|
||||
const content = (args.content as string) ?? "";
|
||||
const tokens = findSensitiveTokens(content);
|
||||
|
||||
@@ -292,7 +332,8 @@ export const explainPermission = (
|
||||
},
|
||||
|
||||
edit: (args) => {
|
||||
const filePath = (args.path as string) ?? (args.file_path as string) ?? "";
|
||||
const filePath =
|
||||
(args.path as string) ?? (args.file_path as string) ?? "";
|
||||
|
||||
return {
|
||||
explanation: `Edit file: ${filePath}`,
|
||||
@@ -304,7 +345,8 @@ export const explainPermission = (
|
||||
},
|
||||
|
||||
read: (args) => {
|
||||
const filePath = (args.path as string) ?? (args.file_path as string) ?? "";
|
||||
const filePath =
|
||||
(args.path as string) ?? (args.file_path as string) ?? "";
|
||||
|
||||
return {
|
||||
explanation: `Read file: ${filePath}`,
|
||||
|
||||
@@ -111,8 +111,8 @@ export const pruneToolOutputs = (
|
||||
// Check for tool messages
|
||||
if (msg.role === "tool") {
|
||||
// Extract tool name from tool_call_id if possible
|
||||
const toolName = (msg as { tool_call_id?: string }).tool_call_id
|
||||
?.split("-")[0] ?? "";
|
||||
const toolName =
|
||||
(msg as { tool_call_id?: string }).tool_call_id?.split("-")[0] ?? "";
|
||||
|
||||
// Skip protected tools
|
||||
if (protectedTools.has(toolName)) {
|
||||
@@ -189,7 +189,10 @@ export const performSessionCompaction = async (
|
||||
const pruneResult = pruneToolOutputs(messages);
|
||||
|
||||
if (pruneResult.prunedCount > 0) {
|
||||
options?.onPruneComplete?.(pruneResult.prunedCount, pruneResult.tokensSaved);
|
||||
options?.onPruneComplete?.(
|
||||
pruneResult.prunedCount,
|
||||
pruneResult.tokensSaved,
|
||||
);
|
||||
|
||||
// Check if pruning was enough
|
||||
const afterPruneCheck = checkCompactionNeeded(pruneResult.messages, config);
|
||||
@@ -236,7 +239,8 @@ export const createCompactionMiddleware = (
|
||||
) => Promise<{ messages: Message[]; summary: string }>;
|
||||
} => {
|
||||
return {
|
||||
shouldCompact: (messages: Message[]) => isContextOverflow(messages, modelId),
|
||||
shouldCompact: (messages: Message[]) =>
|
||||
isContextOverflow(messages, modelId),
|
||||
|
||||
compact: async (messages: Message[]) => {
|
||||
// Notify UI that compaction is starting
|
||||
|
||||
@@ -62,9 +62,10 @@ const generateCommitMessage = (messages: SessionMessage[]): string => {
|
||||
const count = messages.length;
|
||||
|
||||
if (userMessages.length === 0) {
|
||||
return COMMIT_MESSAGE_TEMPLATES.DEFAULT
|
||||
.replace("{summary}", "session checkpoint")
|
||||
.replace("{count}", String(count));
|
||||
return COMMIT_MESSAGE_TEMPLATES.DEFAULT.replace(
|
||||
"{summary}",
|
||||
"session checkpoint",
|
||||
).replace("{count}", String(count));
|
||||
}
|
||||
|
||||
// Get first user message as summary base
|
||||
@@ -77,7 +78,10 @@ const generateCommitMessage = (messages: SessionMessage[]): string => {
|
||||
for (const [type, keywords] of Object.entries(COMMIT_TYPE_KEYWORDS)) {
|
||||
for (const keyword of keywords) {
|
||||
if (allContent.includes(keyword)) {
|
||||
const template = COMMIT_MESSAGE_TEMPLATES[type as keyof typeof COMMIT_MESSAGE_TEMPLATES];
|
||||
const template =
|
||||
COMMIT_MESSAGE_TEMPLATES[
|
||||
type as keyof typeof COMMIT_MESSAGE_TEMPLATES
|
||||
];
|
||||
return template
|
||||
.replace("{summary}", summary || keyword)
|
||||
.replace("{count}", String(count));
|
||||
@@ -85,9 +89,10 @@ const generateCommitMessage = (messages: SessionMessage[]): string => {
|
||||
}
|
||||
}
|
||||
|
||||
return COMMIT_MESSAGE_TEMPLATES.DEFAULT
|
||||
.replace("{summary}", summary || "session changes")
|
||||
.replace("{count}", String(count));
|
||||
return COMMIT_MESSAGE_TEMPLATES.DEFAULT.replace(
|
||||
"{summary}",
|
||||
summary || "session changes",
|
||||
).replace("{count}", String(count));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -124,7 +129,7 @@ const createEmptyForkFile = (sessionId: string): SessionForkFile => {
|
||||
*/
|
||||
const loadForkFile = async (
|
||||
sessionId: string,
|
||||
workingDir: string
|
||||
workingDir: string,
|
||||
): Promise<SessionForkFile> => {
|
||||
const filePath = getForkFilePath(sessionId, workingDir);
|
||||
|
||||
@@ -142,7 +147,7 @@ const loadForkFile = async (
|
||||
*/
|
||||
const saveForkFile = async (
|
||||
file: SessionForkFile,
|
||||
filePath: string
|
||||
filePath: string,
|
||||
): Promise<void> => {
|
||||
const dir = dirname(filePath);
|
||||
|
||||
@@ -160,7 +165,7 @@ const saveForkFile = async (
|
||||
*/
|
||||
export const initializeForkService = async (
|
||||
sessionId: string,
|
||||
workingDir: string
|
||||
workingDir: string,
|
||||
): Promise<void> => {
|
||||
const filePath = getForkFilePath(sessionId, workingDir);
|
||||
const file = await loadForkFile(sessionId, workingDir);
|
||||
@@ -176,7 +181,9 @@ export const initializeForkService = async (
|
||||
*/
|
||||
const getCurrentFork = (): SessionFork | null => {
|
||||
if (!state.file) return null;
|
||||
return state.file.forks.find((f) => f.id === state.file?.currentForkId) || null;
|
||||
return (
|
||||
state.file.forks.find((f) => f.id === state.file?.currentForkId) || null
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -186,8 +193,13 @@ export const createSnapshot = async (
|
||||
messages: SessionMessage[],
|
||||
todoItems: TodoItem[],
|
||||
contextFiles: string[],
|
||||
metadata: { provider: string; model: string; agent: string; workingDir: string },
|
||||
options: SnapshotOptions = {}
|
||||
metadata: {
|
||||
provider: string;
|
||||
model: string;
|
||||
agent: string;
|
||||
workingDir: string;
|
||||
},
|
||||
options: SnapshotOptions = {},
|
||||
): Promise<SnapshotCreateResult> => {
|
||||
if (!state.file || !state.filePath) {
|
||||
return { success: false, error: FORK_ERRORS.SESSION_NOT_FOUND };
|
||||
@@ -203,7 +215,8 @@ export const createSnapshot = async (
|
||||
}
|
||||
|
||||
// Generate snapshot name
|
||||
const name = options.name || `${DEFAULT_SNAPSHOT_PREFIX}-${fork.snapshots.length + 1}`;
|
||||
const name =
|
||||
options.name || `${DEFAULT_SNAPSHOT_PREFIX}-${fork.snapshots.length + 1}`;
|
||||
|
||||
// Check for duplicate name
|
||||
if (fork.snapshots.some((s) => s.name === name)) {
|
||||
@@ -213,7 +226,8 @@ export const createSnapshot = async (
|
||||
const snapshotState: SessionSnapshotState = {
|
||||
messages: [...messages],
|
||||
todoItems: options.includeTodos !== false ? [...todoItems] : [],
|
||||
contextFiles: options.includeContextFiles !== false ? [...contextFiles] : [],
|
||||
contextFiles:
|
||||
options.includeContextFiles !== false ? [...contextFiles] : [],
|
||||
metadata,
|
||||
};
|
||||
|
||||
@@ -240,19 +254,31 @@ export const createSnapshot = async (
|
||||
* Rewind to a snapshot
|
||||
*/
|
||||
export const rewindToSnapshot = async (
|
||||
target: string | number
|
||||
target: string | number,
|
||||
): Promise<RewindResult> => {
|
||||
if (!state.file || !state.filePath) {
|
||||
return { success: false, messagesRestored: 0, error: FORK_ERRORS.SESSION_NOT_FOUND };
|
||||
return {
|
||||
success: false,
|
||||
messagesRestored: 0,
|
||||
error: FORK_ERRORS.SESSION_NOT_FOUND,
|
||||
};
|
||||
}
|
||||
|
||||
const fork = getCurrentFork();
|
||||
if (!fork) {
|
||||
return { success: false, messagesRestored: 0, error: FORK_ERRORS.FORK_NOT_FOUND };
|
||||
return {
|
||||
success: false,
|
||||
messagesRestored: 0,
|
||||
error: FORK_ERRORS.FORK_NOT_FOUND,
|
||||
};
|
||||
}
|
||||
|
||||
if (fork.snapshots.length === 0) {
|
||||
return { success: false, messagesRestored: 0, error: FORK_ERRORS.NO_SNAPSHOTS_TO_REWIND };
|
||||
return {
|
||||
success: false,
|
||||
messagesRestored: 0,
|
||||
error: FORK_ERRORS.NO_SNAPSHOTS_TO_REWIND,
|
||||
};
|
||||
}
|
||||
|
||||
let snapshot: SessionSnapshot | undefined;
|
||||
@@ -260,7 +286,7 @@ export const rewindToSnapshot = async (
|
||||
if (typeof target === "number") {
|
||||
// Rewind by count (e.g., 1 = previous snapshot)
|
||||
const currentIndex = fork.snapshots.findIndex(
|
||||
(s) => s.id === fork.currentSnapshotId
|
||||
(s) => s.id === fork.currentSnapshotId,
|
||||
);
|
||||
const targetIndex = currentIndex - target;
|
||||
|
||||
@@ -275,7 +301,11 @@ export const rewindToSnapshot = async (
|
||||
}
|
||||
|
||||
if (!snapshot) {
|
||||
return { success: false, messagesRestored: 0, error: FORK_ERRORS.SNAPSHOT_NOT_FOUND };
|
||||
return {
|
||||
success: false,
|
||||
messagesRestored: 0,
|
||||
error: FORK_ERRORS.SNAPSHOT_NOT_FOUND,
|
||||
};
|
||||
}
|
||||
|
||||
fork.currentSnapshotId = snapshot.id;
|
||||
@@ -295,7 +325,7 @@ export const rewindToSnapshot = async (
|
||||
* Create a new fork
|
||||
*/
|
||||
export const createFork = async (
|
||||
options: ForkOptions = {}
|
||||
options: ForkOptions = {},
|
||||
): Promise<ForkCreateResult> => {
|
||||
if (!state.file || !state.filePath) {
|
||||
return { success: false, error: FORK_ERRORS.SESSION_NOT_FOUND };
|
||||
@@ -322,7 +352,7 @@ export const createFork = async (
|
||||
let branchFromId = currentFork.currentSnapshotId;
|
||||
if (options.fromSnapshot) {
|
||||
const snapshot = currentFork.snapshots.find(
|
||||
(s) => s.name === options.fromSnapshot || s.id === options.fromSnapshot
|
||||
(s) => s.name === options.fromSnapshot || s.id === options.fromSnapshot,
|
||||
);
|
||||
if (!snapshot) {
|
||||
return { success: false, error: FORK_ERRORS.SNAPSHOT_NOT_FOUND };
|
||||
@@ -331,11 +361,15 @@ export const createFork = async (
|
||||
}
|
||||
|
||||
// Copy snapshots up to branch point
|
||||
const branchIndex = currentFork.snapshots.findIndex((s) => s.id === branchFromId);
|
||||
const copiedSnapshots = currentFork.snapshots.slice(0, branchIndex + 1).map((s) => ({
|
||||
...s,
|
||||
id: uuidv4(), // New IDs for copied snapshots
|
||||
}));
|
||||
const branchIndex = currentFork.snapshots.findIndex(
|
||||
(s) => s.id === branchFromId,
|
||||
);
|
||||
const copiedSnapshots = currentFork.snapshots
|
||||
.slice(0, branchIndex + 1)
|
||||
.map((s) => ({
|
||||
...s,
|
||||
id: uuidv4(), // New IDs for copied snapshots
|
||||
}));
|
||||
|
||||
const newFork: SessionFork = {
|
||||
id: uuidv4(),
|
||||
@@ -385,7 +419,7 @@ export const listForks = (): ForkSummary[] => {
|
||||
|
||||
return state.file.forks.map((fork) => {
|
||||
const currentSnapshot = fork.snapshots.find(
|
||||
(s) => s.id === fork.currentSnapshotId
|
||||
(s) => s.id === fork.currentSnapshotId,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -434,7 +468,9 @@ export const getSnapshot = (nameOrId: string): SessionSnapshot | null => {
|
||||
const fork = getCurrentFork();
|
||||
if (!fork) return null;
|
||||
|
||||
return fork.snapshots.find((s) => s.name === nameOrId || s.id === nameOrId) || null;
|
||||
return (
|
||||
fork.snapshots.find((s) => s.name === nameOrId || s.id === nameOrId) || null
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,9 @@ import type {
|
||||
/**
|
||||
* Parse YAML-like frontmatter from SKILL.md content
|
||||
*/
|
||||
const parseFrontmatter = (content: string): { frontmatter: string; body: string } => {
|
||||
const parseFrontmatter = (
|
||||
content: string,
|
||||
): { frontmatter: string; body: string } => {
|
||||
const delimiter = SKILL_FILE.FRONTMATTER_DELIMITER;
|
||||
const lines = content.split("\n");
|
||||
|
||||
@@ -52,7 +54,10 @@ const parseFrontmatter = (content: string): { frontmatter: string; body: string
|
||||
}
|
||||
|
||||
const frontmatter = lines.slice(1, endIndex).join("\n");
|
||||
const body = lines.slice(endIndex + 1).join("\n").trim();
|
||||
const body = lines
|
||||
.slice(endIndex + 1)
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
return { frontmatter, body };
|
||||
};
|
||||
@@ -141,7 +146,9 @@ const validateFrontmatter = (
|
||||
|
||||
// Validate triggers is an array
|
||||
if (!Array.isArray(data.triggers)) {
|
||||
throw new Error(SKILL_ERRORS.MISSING_REQUIRED_FIELD("triggers (array)", filePath));
|
||||
throw new Error(
|
||||
SKILL_ERRORS.MISSING_REQUIRED_FIELD("triggers (array)", filePath),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -151,7 +158,8 @@ const validateFrontmatter = (
|
||||
version: data.version ? String(data.version) : undefined,
|
||||
triggers: data.triggers as string[],
|
||||
triggerType: data.triggerType as SkillFrontmatter["triggerType"],
|
||||
autoTrigger: typeof data.autoTrigger === "boolean" ? data.autoTrigger : undefined,
|
||||
autoTrigger:
|
||||
typeof data.autoTrigger === "boolean" ? data.autoTrigger : undefined,
|
||||
requiredTools: Array.isArray(data.requiredTools)
|
||||
? (data.requiredTools as string[])
|
||||
: undefined,
|
||||
@@ -206,7 +214,9 @@ const parseExamples = (body: string): SkillExample[] => {
|
||||
/**
|
||||
* Load and parse a SKILL.md file
|
||||
*/
|
||||
export const loadSkillFile = async (filePath: string): Promise<ParsedSkillFile> => {
|
||||
export const loadSkillFile = async (
|
||||
filePath: string,
|
||||
): Promise<ParsedSkillFile> => {
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.size > SKILL_LOADING.MAX_FILE_SIZE_BYTES) {
|
||||
@@ -247,7 +257,8 @@ export const toSkillMetadata = (parsed: ParsedSkillFile): SkillMetadata => ({
|
||||
triggers: parsed.frontmatter.triggers,
|
||||
triggerType: parsed.frontmatter.triggerType ?? SKILL_DEFAULTS.TRIGGER_TYPE,
|
||||
autoTrigger: parsed.frontmatter.autoTrigger ?? SKILL_DEFAULTS.AUTO_TRIGGER,
|
||||
requiredTools: parsed.frontmatter.requiredTools ?? SKILL_DEFAULTS.REQUIRED_TOOLS,
|
||||
requiredTools:
|
||||
parsed.frontmatter.requiredTools ?? SKILL_DEFAULTS.REQUIRED_TOOLS,
|
||||
tags: parsed.frontmatter.tags,
|
||||
});
|
||||
|
||||
@@ -272,7 +283,9 @@ export const toSkillDefinition = (parsed: ParsedSkillFile): SkillDefinition => {
|
||||
/**
|
||||
* Parse skill body to extract system prompt and instructions
|
||||
*/
|
||||
const parseSkillBody = (body: string): { systemPrompt: string; instructions: string } => {
|
||||
const parseSkillBody = (
|
||||
body: string,
|
||||
): { systemPrompt: string; instructions: string } => {
|
||||
// Look for ## System Prompt section
|
||||
const systemPromptMatch = body.match(
|
||||
/## System Prompt([\s\S]*?)(?=## Instructions|## Examples|$)/i,
|
||||
@@ -285,7 +298,9 @@ const parseSkillBody = (body: string): { systemPrompt: string; instructions: str
|
||||
|
||||
// If no sections found, use the whole body as instructions
|
||||
const systemPrompt = systemPromptMatch ? systemPromptMatch[1].trim() : "";
|
||||
const instructions = instructionsMatch ? instructionsMatch[1].trim() : body.trim();
|
||||
const instructions = instructionsMatch
|
||||
? instructionsMatch[1].trim()
|
||||
: body.trim();
|
||||
|
||||
return { systemPrompt, instructions };
|
||||
};
|
||||
|
||||
@@ -5,15 +5,8 @@
|
||||
* Uses progressive disclosure to load skills on demand.
|
||||
*/
|
||||
|
||||
import {
|
||||
SKILL_MATCHING,
|
||||
SKILL_LOADING,
|
||||
SKILL_ERRORS,
|
||||
} from "@constants/skills";
|
||||
import {
|
||||
loadAllSkills,
|
||||
loadSkillById,
|
||||
} from "@services/skill-loader";
|
||||
import { SKILL_MATCHING, SKILL_LOADING, SKILL_ERRORS } from "@constants/skills";
|
||||
import { loadAllSkills, loadSkillById } from "@services/skill-loader";
|
||||
import type {
|
||||
SkillDefinition,
|
||||
SkillMatch,
|
||||
@@ -175,7 +168,9 @@ const matchTrigger = (
|
||||
/**
|
||||
* Find matching skills for user input
|
||||
*/
|
||||
export const findMatchingSkills = async (input: string): Promise<SkillMatch[]> => {
|
||||
export const findMatchingSkills = async (
|
||||
input: string,
|
||||
): Promise<SkillMatch[]> => {
|
||||
await refreshIfNeeded();
|
||||
|
||||
const matches: SkillMatch[] = [];
|
||||
@@ -213,7 +208,9 @@ export const findMatchingSkills = async (input: string): Promise<SkillMatch[]> =
|
||||
/**
|
||||
* Find the best matching skill for input
|
||||
*/
|
||||
export const findBestMatch = async (input: string): Promise<SkillMatch | null> => {
|
||||
export const findBestMatch = async (
|
||||
input: string,
|
||||
): Promise<SkillMatch | null> => {
|
||||
const matches = await findMatchingSkills(input);
|
||||
return matches.length > 0 ? matches[0] : null;
|
||||
};
|
||||
@@ -382,8 +379,8 @@ export const getAutoTriggerSkills = (): SkillDefinition[] => {
|
||||
* Get skills by tag
|
||||
*/
|
||||
export const getSkillsByTag = (tag: string): SkillDefinition[] => {
|
||||
return Array.from(registryState.skills.values()).filter(
|
||||
(skill) => skill.tags?.includes(tag),
|
||||
return Array.from(registryState.skills.values()).filter((skill) =>
|
||||
skill.tags?.includes(tag),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -185,7 +185,8 @@ export const createSnapshot = async (
|
||||
|
||||
const id = uuidv4();
|
||||
const timestamp = Date.now();
|
||||
const snapshotMessage = message ?? `Snapshot ${new Date(timestamp).toISOString()}`;
|
||||
const snapshotMessage =
|
||||
message ?? `Snapshot ${new Date(timestamp).toISOString()}`;
|
||||
|
||||
// Get current state
|
||||
const currentCommit = getCurrentCommitHash(workingDir);
|
||||
@@ -253,7 +254,11 @@ export const getSnapshot = async (
|
||||
workingDir: string,
|
||||
snapshotId: string,
|
||||
): Promise<Snapshot | null> => {
|
||||
const snapshotPath = path.join(workingDir, SNAPSHOTS_DIR, `${snapshotId}.json`);
|
||||
const snapshotPath = path.join(
|
||||
workingDir,
|
||||
SNAPSHOTS_DIR,
|
||||
`${snapshotId}.json`,
|
||||
);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(snapshotPath, "utf-8");
|
||||
@@ -263,7 +268,9 @@ export const getSnapshot = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const listSnapshots = async (workingDir: string): Promise<SnapshotMetadata[]> => {
|
||||
export const listSnapshots = async (
|
||||
workingDir: string,
|
||||
): Promise<SnapshotMetadata[]> => {
|
||||
const snapshotsDir = path.join(workingDir, SNAPSHOTS_DIR);
|
||||
|
||||
if (!(await fileExists(snapshotsDir))) {
|
||||
@@ -278,7 +285,10 @@ export const listSnapshots = async (workingDir: string): Promise<SnapshotMetadat
|
||||
if (!file.endsWith(".json")) continue;
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(path.join(snapshotsDir, file), "utf-8");
|
||||
const content = await fs.readFile(
|
||||
path.join(snapshotsDir, file),
|
||||
"utf-8",
|
||||
);
|
||||
const snapshot = JSON.parse(content) as Snapshot;
|
||||
snapshots.push({
|
||||
id: snapshot.id,
|
||||
@@ -302,7 +312,11 @@ export const deleteSnapshot = async (
|
||||
workingDir: string,
|
||||
snapshotId: string,
|
||||
): Promise<boolean> => {
|
||||
const snapshotPath = path.join(workingDir, SNAPSHOTS_DIR, `${snapshotId}.json`);
|
||||
const snapshotPath = path.join(
|
||||
workingDir,
|
||||
SNAPSHOTS_DIR,
|
||||
`${snapshotId}.json`,
|
||||
);
|
||||
|
||||
try {
|
||||
await fs.unlink(snapshotPath);
|
||||
@@ -312,7 +326,9 @@ export const deleteSnapshot = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const pruneOldSnapshots = async (workingDir: string): Promise<number> => {
|
||||
export const pruneOldSnapshots = async (
|
||||
workingDir: string,
|
||||
): Promise<number> => {
|
||||
const cutoff = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||
const snapshots = await listSnapshots(workingDir);
|
||||
let deleted = 0;
|
||||
@@ -351,7 +367,11 @@ export const validatePatch = async (
|
||||
patch: string,
|
||||
): Promise<{ valid: boolean; errors: string[] }> => {
|
||||
// Write patch to temp file
|
||||
const tempPatchPath = path.join(workingDir, SNAPSHOTS_DIR, `temp-${Date.now()}.patch`);
|
||||
const tempPatchPath = path.join(
|
||||
workingDir,
|
||||
SNAPSHOTS_DIR,
|
||||
`temp-${Date.now()}.patch`,
|
||||
);
|
||||
|
||||
try {
|
||||
await fs.writeFile(tempPatchPath, patch);
|
||||
|
||||
Reference in New Issue
Block a user