fixing imports

This commit is contained in:
2026-02-04 21:32:30 -05:00
parent 74b0a0dbab
commit db79856b08
166 changed files with 1986 additions and 982 deletions

View File

@@ -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}`;
};

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
}));

View File

@@ -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

View File

@@ -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) {

View File

@@ -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 (

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 => {

View File

@@ -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";

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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";

View File

@@ -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;
}

View File

@@ -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"] },
];

View File

@@ -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;

View File

@@ -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}`);

View File

@@ -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";

View File

@@ -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";
/**

View File

@@ -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";

View File

@@ -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" }),
};

View File

@@ -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}`);
}

View File

@@ -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([

View File

@@ -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("");

View File

@@ -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);

View File

@@ -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 [];

View File

@@ -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);

View File

@@ -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[] => {

View File

@@ -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;
};

View File

@@ -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);
};
/**

View File

@@ -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"],
};
/**

View File

@@ -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;
});
};
/**

View File

@@ -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();
}

View File

@@ -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,
};
};

View File

@@ -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;

View File

@@ -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,

View File

@@ -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),
);
},
};

View File

@@ -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

View File

@@ -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);
};

View File

@@ -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

View File

@@ -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>();

View File

@@ -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);
};

View File

@@ -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: [],

View File

@@ -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 },
],
},

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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)) {

View File

@@ -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,
},

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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");

View File

@@ -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");

View File

@@ -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];

View File

@@ -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;

View File

@@ -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

View File

@@ -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}`,

View File

@@ -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

View File

@@ -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
);
};
/**

View File

@@ -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 };
};

View File

@@ -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),
);
};

View File

@@ -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);