Fixing the prompts
This commit is contained in:
@@ -325,7 +325,14 @@ export const runAgentLoopStream = async (
|
||||
|
||||
// Initialize
|
||||
await initializePermissions();
|
||||
await refreshMCPTools();
|
||||
|
||||
// Refresh MCP tools and log results
|
||||
const mcpResult = await refreshMCPTools();
|
||||
if (mcpResult.success && mcpResult.toolCount > 0) {
|
||||
state.options.onText?.(`[Loaded ${mcpResult.toolCount} MCP tool(s)]\n`);
|
||||
} else if (mcpResult.error) {
|
||||
state.options.onWarning?.(`MCP tools unavailable: ${mcpResult.error}`);
|
||||
}
|
||||
|
||||
const agentMessages: AgentMessage[] = [...messages];
|
||||
|
||||
|
||||
@@ -11,24 +11,24 @@ import {
|
||||
} from "@services/session";
|
||||
import { getConfig } from "@services/config";
|
||||
import { initializePermissions } from "@services/permissions";
|
||||
import { projectConfig } from "@services/project-config";
|
||||
import { getProviderStatus } from "@providers/index";
|
||||
import { appStore } from "@tui/index";
|
||||
import { themeActions } from "@stores/theme-store";
|
||||
import {
|
||||
AGENTIC_SYSTEM_PROMPT,
|
||||
buildAgenticPrompt,
|
||||
buildSystemPromptWithRules,
|
||||
} from "@prompts/index";
|
||||
buildBaseContext,
|
||||
buildCompletePrompt,
|
||||
} from "@services/prompt-builder";
|
||||
import { initSuggestionService } from "@services/command-suggestion-service";
|
||||
import { addContextFile } from "@services/chat-tui/files";
|
||||
import type { ProviderName, Message } from "@/types/providers";
|
||||
import type { ChatSession } from "@/types/index";
|
||||
import type { ChatTUIOptions } from "@interfaces/ChatTUIOptions";
|
||||
import type { ChatServiceState } from "@/types/chat-service";
|
||||
import type { InteractionMode } from "@/types/tui";
|
||||
|
||||
const createInitialState = async (
|
||||
options: ChatTUIOptions,
|
||||
initialMode: InteractionMode = "agent",
|
||||
): Promise<ChatServiceState> => {
|
||||
const config = await getConfig();
|
||||
|
||||
@@ -37,7 +37,8 @@ const createInitialState = async (
|
||||
model: options.model || config.get("model") || undefined,
|
||||
messages: [],
|
||||
contextFiles: new Map(),
|
||||
systemPrompt: AGENTIC_SYSTEM_PROMPT,
|
||||
systemPrompt: "",
|
||||
currentMode: initialMode,
|
||||
verbose: options.verbose || false,
|
||||
autoApprove: options.autoApprove || false,
|
||||
};
|
||||
@@ -52,26 +53,6 @@ const validateProvider = async (state: ChatServiceState): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
const getGitContext = async (): Promise<{
|
||||
isGitRepo: boolean;
|
||||
branch?: string;
|
||||
status?: string;
|
||||
recentCommits?: string[];
|
||||
}> => {
|
||||
try {
|
||||
const { execSync } = await import("child_process");
|
||||
const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim();
|
||||
const status = execSync("git status --short", { encoding: "utf-8" }).trim() || "(clean)";
|
||||
const commits = execSync("git log --oneline -5", { encoding: "utf-8" })
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
return { isGitRepo: true, branch, status, recentCommits: commits };
|
||||
} catch {
|
||||
return { isGitRepo: false };
|
||||
}
|
||||
};
|
||||
|
||||
const buildSystemPrompt = async (
|
||||
state: ChatServiceState,
|
||||
options: ChatTUIOptions,
|
||||
@@ -81,23 +62,14 @@ const buildSystemPrompt = async (
|
||||
return;
|
||||
}
|
||||
|
||||
// Build agentic prompt with environment context
|
||||
const gitContext = await getGitContext();
|
||||
const basePrompt = buildAgenticPrompt({
|
||||
workingDir: process.cwd(),
|
||||
isGitRepo: gitContext.isGitRepo,
|
||||
platform: process.platform,
|
||||
today: new Date().toISOString().split("T")[0],
|
||||
model: state.model,
|
||||
gitBranch: gitContext.branch,
|
||||
gitStatus: gitContext.status,
|
||||
recentCommits: gitContext.recentCommits,
|
||||
});
|
||||
const context = await buildBaseContext(state.model);
|
||||
const { prompt, rulesPaths } = await buildCompletePrompt(
|
||||
state.currentMode,
|
||||
context,
|
||||
options.appendSystemPrompt,
|
||||
);
|
||||
|
||||
// Add project rules
|
||||
const { prompt: promptWithRules, rulesPaths } =
|
||||
await buildSystemPromptWithRules(basePrompt, process.cwd());
|
||||
state.systemPrompt = promptWithRules;
|
||||
state.systemPrompt = prompt;
|
||||
|
||||
if (rulesPaths.length > 0 && state.verbose) {
|
||||
infoMessage(`Loaded ${rulesPaths.length} rule file(s):`);
|
||||
@@ -105,19 +77,6 @@ const buildSystemPrompt = async (
|
||||
infoMessage(` - ${rulePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
const learningsContext = await projectConfig.buildLearningsContext();
|
||||
if (learningsContext) {
|
||||
state.systemPrompt = state.systemPrompt + "\n\n" + learningsContext;
|
||||
if (state.verbose) {
|
||||
infoMessage("Loaded project learnings");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.appendSystemPrompt) {
|
||||
state.systemPrompt =
|
||||
state.systemPrompt + "\n\n" + options.appendSystemPrompt;
|
||||
}
|
||||
};
|
||||
|
||||
const restoreMessagesFromSession = (
|
||||
@@ -188,10 +147,36 @@ const initializeTheme = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Rebuild system prompt when interaction mode changes
|
||||
* Updates both the state and the first message in the conversation
|
||||
*/
|
||||
export const rebuildSystemPromptForMode = async (
|
||||
state: ChatServiceState,
|
||||
newMode: InteractionMode,
|
||||
appendPrompt?: string,
|
||||
): Promise<void> => {
|
||||
if (state.currentMode === newMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentMode = newMode;
|
||||
|
||||
const context = await buildBaseContext(state.model);
|
||||
const { prompt } = await buildCompletePrompt(newMode, context, appendPrompt);
|
||||
|
||||
state.systemPrompt = prompt;
|
||||
|
||||
if (state.messages.length > 0 && state.messages[0].role === "system") {
|
||||
state.messages[0].content = prompt;
|
||||
}
|
||||
};
|
||||
|
||||
export const initializeChatService = async (
|
||||
options: ChatTUIOptions,
|
||||
): Promise<{ state: ChatServiceState; session: ChatSession }> => {
|
||||
const state = await createInitialState(options);
|
||||
const initialMode = appStore.getState().interactionMode;
|
||||
const state = await createInitialState(options, initialMode);
|
||||
|
||||
await validateProvider(state);
|
||||
await buildSystemPrompt(state, options);
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "@services/chat-tui/files";
|
||||
import { getToolDescription } from "@services/chat-tui/utils";
|
||||
import { processLearningsFromExchange } from "@services/chat-tui/learnings";
|
||||
import { rebuildSystemPromptForMode } from "@services/chat-tui/initialize";
|
||||
import {
|
||||
compactConversation,
|
||||
createCompactionSummary,
|
||||
@@ -61,6 +62,10 @@ import {
|
||||
detectCommand,
|
||||
executeDetectedCommand,
|
||||
} from "@services/command-detection";
|
||||
import {
|
||||
detectSkillCommand,
|
||||
executeSkill,
|
||||
} from "@services/skill-service";
|
||||
|
||||
// Track last response for feedback learning
|
||||
let lastResponseContext: {
|
||||
@@ -276,6 +281,30 @@ export const handleMessage = async (
|
||||
// Check for feedback on previous response
|
||||
await checkUserFeedback(message, callbacks);
|
||||
|
||||
// Check for skill commands (e.g., /review, /commit)
|
||||
const skillMatch = await detectSkillCommand(message);
|
||||
if (skillMatch) {
|
||||
addDebugLog("info", `Detected skill: /${skillMatch.skill.command}`);
|
||||
callbacks.onLog(
|
||||
"system",
|
||||
`Running skill: ${skillMatch.skill.name}`,
|
||||
);
|
||||
|
||||
// Execute the skill and get the expanded prompt
|
||||
const { expandedPrompt } = executeSkill(skillMatch.skill, skillMatch.args);
|
||||
|
||||
// Show the original command
|
||||
appStore.addLog({
|
||||
type: "user",
|
||||
content: message,
|
||||
});
|
||||
|
||||
// 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)}...`);
|
||||
}
|
||||
|
||||
// Detect explicit command requests and execute directly
|
||||
const detected = detectCommand(message);
|
||||
if (detected.detected && detected.command) {
|
||||
@@ -324,11 +353,20 @@ export const handleMessage = async (
|
||||
const { interactionMode, cascadeEnabled } = appStore.getState();
|
||||
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`,
|
||||
);
|
||||
}
|
||||
|
||||
if (isReadOnlyMode) {
|
||||
const modeLabel = interactionMode === "ask" ? "Ask" : "Code Review";
|
||||
callbacks.onLog(
|
||||
"system",
|
||||
`${modeLabel} mode: Read-only responses (Ctrl+Tab to switch modes)`,
|
||||
`${modeLabel} mode: Read-only tools only (Ctrl+Tab to switch modes)`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
263
src/services/context-gathering.ts
Normal file
263
src/services/context-gathering.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Context Gathering Service
|
||||
*
|
||||
* Automatically gathers project context for ask mode.
|
||||
* Provides codebase overview without requiring manual file references.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
export interface ProjectContext {
|
||||
projectType: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
mainLanguage: string;
|
||||
frameworks: string[];
|
||||
structure: string;
|
||||
keyFiles: string[];
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
interface ProjectConfig {
|
||||
name?: string;
|
||||
description?: string;
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
scripts?: Record<string, string>;
|
||||
}
|
||||
|
||||
const PROJECT_MARKERS: Record<string, { type: string; language: string }> = {
|
||||
"package.json": { type: "Node.js", language: "JavaScript/TypeScript" },
|
||||
"tsconfig.json": { type: "TypeScript", language: "TypeScript" },
|
||||
"Cargo.toml": { type: "Rust", language: "Rust" },
|
||||
"go.mod": { type: "Go", language: "Go" },
|
||||
"pom.xml": { type: "Maven/Java", language: "Java" },
|
||||
"build.gradle": { type: "Gradle/Java", language: "Java" },
|
||||
"pyproject.toml": { type: "Python", language: "Python" },
|
||||
"setup.py": { type: "Python", language: "Python" },
|
||||
"requirements.txt": { type: "Python", language: "Python" },
|
||||
"Gemfile": { type: "Ruby", language: "Ruby" },
|
||||
"composer.json": { type: "PHP", language: "PHP" },
|
||||
".csproj": { type: ".NET", language: "C#" },
|
||||
};
|
||||
|
||||
const FRAMEWORK_MARKERS: Record<string, string[]> = {
|
||||
react: ["react", "react-dom", "next", "gatsby"],
|
||||
vue: ["vue", "nuxt"],
|
||||
angular: ["@angular/core"],
|
||||
svelte: ["svelte", "sveltekit"],
|
||||
express: ["express"],
|
||||
fastify: ["fastify"],
|
||||
nestjs: ["@nestjs/core"],
|
||||
django: ["django"],
|
||||
flask: ["flask"],
|
||||
rails: ["rails"],
|
||||
spring: ["spring-boot"],
|
||||
};
|
||||
|
||||
const IGNORED_DIRS = new Set([
|
||||
"node_modules",
|
||||
".git",
|
||||
"dist",
|
||||
"build",
|
||||
".next",
|
||||
".nuxt",
|
||||
"target",
|
||||
"__pycache__",
|
||||
".venv",
|
||||
"venv",
|
||||
"vendor",
|
||||
".idea",
|
||||
".vscode",
|
||||
"coverage",
|
||||
]);
|
||||
|
||||
const detectProjectType = (workingDir: string): { type: string; language: string } => {
|
||||
for (const [marker, info] of Object.entries(PROJECT_MARKERS)) {
|
||||
if (existsSync(join(workingDir, marker))) {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
return { type: "Unknown", language: "Unknown" };
|
||||
};
|
||||
|
||||
const detectFrameworks = (deps: Record<string, string>): string[] => {
|
||||
const frameworks: string[] = [];
|
||||
|
||||
for (const [framework, markers] of Object.entries(FRAMEWORK_MARKERS)) {
|
||||
for (const marker of markers) {
|
||||
if (deps[marker]) {
|
||||
frameworks.push(framework);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frameworks;
|
||||
};
|
||||
|
||||
const readPackageJson = (workingDir: string): ProjectConfig | null => {
|
||||
const packagePath = join(workingDir, "package.json");
|
||||
if (!existsSync(packagePath)) return null;
|
||||
|
||||
try {
|
||||
const content = readFileSync(packagePath, "utf-8");
|
||||
return JSON.parse(content) as ProjectConfig;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getDirectoryStructure = (
|
||||
dir: string,
|
||||
baseDir: string,
|
||||
depth = 0,
|
||||
maxDepth = 3,
|
||||
): string[] => {
|
||||
if (depth >= maxDepth) return [];
|
||||
|
||||
const entries: string[] = [];
|
||||
try {
|
||||
const items = readdirSync(dir);
|
||||
|
||||
for (const item of items) {
|
||||
if (IGNORED_DIRS.has(item) || item.startsWith(".")) continue;
|
||||
|
||||
const fullPath = join(dir, item);
|
||||
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
const indent = " ".repeat(depth);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
entries.push(`${indent}${item}/`);
|
||||
const subEntries = getDirectoryStructure(fullPath, baseDir, depth + 1, maxDepth);
|
||||
entries.push(...subEntries);
|
||||
} else if (depth < 2) {
|
||||
entries.push(`${indent}${item}`);
|
||||
}
|
||||
} catch {
|
||||
// Skip inaccessible files
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip inaccessible directories
|
||||
}
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
||||
const getKeyFiles = (workingDir: string): string[] => {
|
||||
const keyPatterns = [
|
||||
"README.md",
|
||||
"readme.md",
|
||||
"README",
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
"Cargo.toml",
|
||||
"go.mod",
|
||||
"pyproject.toml",
|
||||
".env.example",
|
||||
"docker-compose.yml",
|
||||
"Dockerfile",
|
||||
"Makefile",
|
||||
];
|
||||
|
||||
const found: string[] = [];
|
||||
for (const pattern of keyPatterns) {
|
||||
if (existsSync(join(workingDir, pattern))) {
|
||||
found.push(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
};
|
||||
|
||||
const getMainDependencies = (pkg: ProjectConfig): string[] => {
|
||||
const allDeps = {
|
||||
...pkg.dependencies,
|
||||
...pkg.devDependencies,
|
||||
};
|
||||
|
||||
const importantDeps = Object.keys(allDeps).filter(
|
||||
(dep) =>
|
||||
!dep.startsWith("@types/") &&
|
||||
!dep.startsWith("eslint") &&
|
||||
!dep.startsWith("prettier") &&
|
||||
!dep.includes("lint"),
|
||||
);
|
||||
|
||||
return importantDeps.slice(0, 15);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gather comprehensive project context
|
||||
*/
|
||||
export const gatherProjectContext = (workingDir: string): ProjectContext => {
|
||||
const { type, language } = detectProjectType(workingDir);
|
||||
const pkg = readPackageJson(workingDir);
|
||||
const structure = getDirectoryStructure(workingDir, workingDir);
|
||||
const keyFiles = getKeyFiles(workingDir);
|
||||
|
||||
const frameworks = pkg
|
||||
? detectFrameworks({ ...pkg.dependencies, ...pkg.devDependencies })
|
||||
: [];
|
||||
|
||||
const dependencies = pkg ? getMainDependencies(pkg) : undefined;
|
||||
|
||||
return {
|
||||
projectType: type,
|
||||
name: pkg?.name,
|
||||
description: pkg?.description,
|
||||
mainLanguage: language,
|
||||
frameworks,
|
||||
structure: structure.slice(0, 50).join("\n"),
|
||||
keyFiles,
|
||||
dependencies,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a formatted context string for injection into prompts
|
||||
*/
|
||||
export const buildProjectContextString = (context: ProjectContext): string => {
|
||||
const sections: string[] = [];
|
||||
|
||||
const header = context.name
|
||||
? `Project: ${context.name}`
|
||||
: `Project Type: ${context.projectType}`;
|
||||
sections.push(header);
|
||||
|
||||
if (context.description) {
|
||||
sections.push(`Description: ${context.description}`);
|
||||
}
|
||||
|
||||
sections.push(`Language: ${context.mainLanguage}`);
|
||||
|
||||
if (context.frameworks.length > 0) {
|
||||
sections.push(`Frameworks: ${context.frameworks.join(", ")}`);
|
||||
}
|
||||
|
||||
if (context.keyFiles.length > 0) {
|
||||
sections.push(`Key Files: ${context.keyFiles.join(", ")}`);
|
||||
}
|
||||
|
||||
if (context.dependencies && context.dependencies.length > 0) {
|
||||
sections.push(`Main Dependencies: ${context.dependencies.join(", ")}`);
|
||||
}
|
||||
|
||||
if (context.structure) {
|
||||
sections.push(`\nProject Structure:\n\`\`\`\n${context.structure}\n\`\`\``);
|
||||
}
|
||||
|
||||
return sections.join("\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Get project context for ask mode prompts
|
||||
*/
|
||||
export const getProjectContextForAskMode = (workingDir: string): string => {
|
||||
const context = gatherProjectContext(workingDir);
|
||||
return buildProjectContextString(context);
|
||||
};
|
||||
249
src/services/prompt-builder.ts
Normal file
249
src/services/prompt-builder.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Prompt Builder Service
|
||||
*
|
||||
* Builds and manages system prompts based on interaction mode.
|
||||
* Handles mode switching and context injection.
|
||||
*/
|
||||
|
||||
import { buildAgenticPrompt } from "@prompts/system/agent";
|
||||
import { buildAskPrompt } from "@prompts/system/ask";
|
||||
import { buildCodeReviewPrompt } from "@prompts/system/code-review";
|
||||
import { buildSystemPromptWithRules } from "@services/rules-service";
|
||||
import { projectConfig } from "@services/project-config";
|
||||
import { getProjectContextForAskMode } from "@services/context-gathering";
|
||||
import type { InteractionMode } from "@/types/tui";
|
||||
|
||||
export interface PromptContext {
|
||||
workingDir: string;
|
||||
isGitRepo: boolean;
|
||||
platform: string;
|
||||
today: string;
|
||||
model?: string;
|
||||
gitBranch?: string;
|
||||
gitStatus?: string;
|
||||
recentCommits?: string[];
|
||||
projectContext?: string;
|
||||
prContext?: string;
|
||||
}
|
||||
|
||||
export interface PromptBuilderState {
|
||||
currentMode: InteractionMode;
|
||||
basePrompt: string;
|
||||
fullPrompt: string;
|
||||
context: PromptContext;
|
||||
}
|
||||
|
||||
const MODE_PROMPT_BUILDERS: Record<
|
||||
InteractionMode,
|
||||
(context: PromptContext) => string
|
||||
> = {
|
||||
agent: (ctx) =>
|
||||
buildAgenticPrompt({
|
||||
workingDir: ctx.workingDir,
|
||||
isGitRepo: ctx.isGitRepo,
|
||||
platform: ctx.platform,
|
||||
today: ctx.today,
|
||||
model: ctx.model,
|
||||
gitBranch: ctx.gitBranch,
|
||||
gitStatus: ctx.gitStatus,
|
||||
recentCommits: ctx.recentCommits,
|
||||
}),
|
||||
|
||||
ask: (ctx) => {
|
||||
const projectContext =
|
||||
ctx.projectContext ?? getProjectContextForAskMode(ctx.workingDir);
|
||||
return buildAskPrompt({
|
||||
workingDir: ctx.workingDir,
|
||||
isGitRepo: ctx.isGitRepo,
|
||||
platform: ctx.platform,
|
||||
today: ctx.today,
|
||||
model: ctx.model,
|
||||
projectContext,
|
||||
});
|
||||
},
|
||||
|
||||
"code-review": (ctx) =>
|
||||
buildCodeReviewPrompt({
|
||||
workingDir: ctx.workingDir,
|
||||
isGitRepo: ctx.isGitRepo,
|
||||
platform: ctx.platform,
|
||||
today: ctx.today,
|
||||
model: ctx.model,
|
||||
prContext: ctx.prContext,
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Get git context for prompt building
|
||||
*/
|
||||
export const getGitContext = async (): Promise<{
|
||||
isGitRepo: boolean;
|
||||
branch?: string;
|
||||
status?: string;
|
||||
recentCommits?: string[];
|
||||
}> => {
|
||||
try {
|
||||
const { execSync } = await import("child_process");
|
||||
const branch = execSync("git branch --show-current", {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
const status =
|
||||
execSync("git status --short", { encoding: "utf-8" }).trim() || "(clean)";
|
||||
const commits = execSync("git log --oneline -5", { encoding: "utf-8" })
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
return { isGitRepo: true, branch, status, recentCommits: commits };
|
||||
} catch {
|
||||
return { isGitRepo: false };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build base context for all modes
|
||||
*/
|
||||
export const buildBaseContext = async (
|
||||
model?: string,
|
||||
): Promise<PromptContext> => {
|
||||
const gitContext = await getGitContext();
|
||||
|
||||
return {
|
||||
workingDir: process.cwd(),
|
||||
isGitRepo: gitContext.isGitRepo,
|
||||
platform: process.platform,
|
||||
today: new Date().toISOString().split("T")[0],
|
||||
model,
|
||||
gitBranch: gitContext.branch,
|
||||
gitStatus: gitContext.status,
|
||||
recentCommits: gitContext.recentCommits,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the base prompt for a specific mode
|
||||
*/
|
||||
export const buildModePrompt = (
|
||||
mode: InteractionMode,
|
||||
context: PromptContext,
|
||||
): string => {
|
||||
const builder = MODE_PROMPT_BUILDERS[mode];
|
||||
return builder(context);
|
||||
};
|
||||
|
||||
/**
|
||||
* Build complete system prompt with rules and learnings
|
||||
*/
|
||||
export const buildCompletePrompt = async (
|
||||
mode: InteractionMode,
|
||||
context: PromptContext,
|
||||
appendPrompt?: string,
|
||||
): Promise<{ prompt: string; rulesPaths: string[] }> => {
|
||||
const basePrompt = buildModePrompt(mode, context);
|
||||
|
||||
const { prompt: promptWithRules, rulesPaths } =
|
||||
await buildSystemPromptWithRules(basePrompt, context.workingDir);
|
||||
|
||||
let finalPrompt = promptWithRules;
|
||||
|
||||
const learningsContext = await projectConfig.buildLearningsContext();
|
||||
if (learningsContext) {
|
||||
finalPrompt = finalPrompt + "\n\n" + learningsContext;
|
||||
}
|
||||
|
||||
if (appendPrompt) {
|
||||
finalPrompt = finalPrompt + "\n\n" + appendPrompt;
|
||||
}
|
||||
|
||||
return { prompt: finalPrompt, rulesPaths };
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a prompt builder instance for managing prompts across mode changes
|
||||
*/
|
||||
export const createPromptBuilder = (initialModel?: string) => {
|
||||
let state: PromptBuilderState | null = null;
|
||||
|
||||
const initialize = async (
|
||||
mode: InteractionMode,
|
||||
appendPrompt?: string,
|
||||
): Promise<string> => {
|
||||
const context = await buildBaseContext(initialModel);
|
||||
const { prompt } = await buildCompletePrompt(mode, context, appendPrompt);
|
||||
|
||||
state = {
|
||||
currentMode: mode,
|
||||
basePrompt: buildModePrompt(mode, context),
|
||||
fullPrompt: prompt,
|
||||
context,
|
||||
};
|
||||
|
||||
return prompt;
|
||||
};
|
||||
|
||||
const switchMode = async (
|
||||
newMode: InteractionMode,
|
||||
appendPrompt?: string,
|
||||
): Promise<string> => {
|
||||
if (!state) {
|
||||
return initialize(newMode, appendPrompt);
|
||||
}
|
||||
|
||||
if (state.currentMode === newMode) {
|
||||
return state.fullPrompt;
|
||||
}
|
||||
|
||||
const { prompt } = await buildCompletePrompt(
|
||||
newMode,
|
||||
state.context,
|
||||
appendPrompt,
|
||||
);
|
||||
|
||||
state = {
|
||||
currentMode: newMode,
|
||||
basePrompt: buildModePrompt(newMode, state.context),
|
||||
fullPrompt: prompt,
|
||||
context: state.context,
|
||||
};
|
||||
|
||||
return prompt;
|
||||
};
|
||||
|
||||
const getCurrentPrompt = (): string | null => state?.fullPrompt ?? null;
|
||||
|
||||
const getCurrentMode = (): InteractionMode | null =>
|
||||
state?.currentMode ?? null;
|
||||
|
||||
const updateContext = async (
|
||||
updates: Partial<PromptContext>,
|
||||
appendPrompt?: string,
|
||||
): Promise<string> => {
|
||||
if (!state) {
|
||||
throw new Error("Prompt builder not initialized");
|
||||
}
|
||||
|
||||
const newContext = { ...state.context, ...updates };
|
||||
const { prompt } = await buildCompletePrompt(
|
||||
state.currentMode,
|
||||
newContext,
|
||||
appendPrompt,
|
||||
);
|
||||
|
||||
state = {
|
||||
...state,
|
||||
context: newContext,
|
||||
fullPrompt: prompt,
|
||||
};
|
||||
|
||||
return prompt;
|
||||
};
|
||||
|
||||
return {
|
||||
initialize,
|
||||
switchMode,
|
||||
getCurrentPrompt,
|
||||
getCurrentMode,
|
||||
updateContext,
|
||||
};
|
||||
};
|
||||
|
||||
export type PromptBuilder = ReturnType<typeof createPromptBuilder>;
|
||||
121
src/services/skill-service.ts
Normal file
121
src/services/skill-service.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Skill Service
|
||||
*
|
||||
* Handles skill detection and execution.
|
||||
* Skills are custom commands that expand into prompts.
|
||||
*/
|
||||
|
||||
import { getSkills } from "@services/project-config";
|
||||
import type { SkillConfig } from "@/types/project-config";
|
||||
|
||||
export interface SkillMatch {
|
||||
skill: SkillConfig;
|
||||
args: string;
|
||||
}
|
||||
|
||||
export interface SkillExecutionResult {
|
||||
expandedPrompt: string;
|
||||
skill: SkillConfig;
|
||||
}
|
||||
|
||||
let skillsCache: SkillConfig[] | null = null;
|
||||
|
||||
/**
|
||||
* Load and cache skills
|
||||
*/
|
||||
export const loadSkills = async (): Promise<SkillConfig[]> => {
|
||||
if (skillsCache) return skillsCache;
|
||||
skillsCache = await getSkills();
|
||||
return skillsCache;
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh skills cache
|
||||
*/
|
||||
export const refreshSkills = async (): Promise<void> => {
|
||||
skillsCache = await getSkills();
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect if a message starts with a skill command
|
||||
* Skills are invoked with /command syntax
|
||||
*/
|
||||
export const detectSkillCommand = async (
|
||||
message: string,
|
||||
): Promise<SkillMatch | null> => {
|
||||
const trimmed = message.trim();
|
||||
|
||||
if (!trimmed.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const skills = await loadSkills();
|
||||
|
||||
for (const skill of skills) {
|
||||
const commandPattern = `/${skill.command}`;
|
||||
|
||||
if (
|
||||
trimmed === commandPattern ||
|
||||
trimmed.startsWith(`${commandPattern} `)
|
||||
) {
|
||||
const args = trimmed.slice(commandPattern.length).trim();
|
||||
return { skill, args };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute a skill by expanding its prompt template
|
||||
* Supports $ARGS placeholder for arguments
|
||||
*/
|
||||
export const executeSkill = (
|
||||
skill: SkillConfig,
|
||||
args: string,
|
||||
): SkillExecutionResult => {
|
||||
let expandedPrompt = skill.prompt;
|
||||
|
||||
// Replace $ARGS placeholder with actual arguments
|
||||
expandedPrompt = expandedPrompt.replace(/\$ARGS/g, args);
|
||||
|
||||
// Replace $1, $2, etc. with positional arguments
|
||||
const argParts = args.split(/\s+/).filter(Boolean);
|
||||
for (let i = 0; i < argParts.length; i++) {
|
||||
expandedPrompt = expandedPrompt.replace(
|
||||
new RegExp(`\\$${i + 1}`, "g"),
|
||||
argParts[i],
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up any remaining unreplaced placeholders
|
||||
expandedPrompt = expandedPrompt.replace(/\$\d+/g, "");
|
||||
expandedPrompt = expandedPrompt.replace(/\$ARGS/g, "");
|
||||
|
||||
return {
|
||||
expandedPrompt: expandedPrompt.trim(),
|
||||
skill,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all available skill commands for display
|
||||
*/
|
||||
export const getAvailableSkills = async (): Promise<
|
||||
Array<{ command: string; name: string; description: string }>
|
||||
> => {
|
||||
const skills = await loadSkills();
|
||||
return skills.map((s) => ({
|
||||
command: `/${s.command}`,
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if skills are available
|
||||
*/
|
||||
export const hasSkills = async (): Promise<boolean> => {
|
||||
const skills = await loadSkills();
|
||||
return skills.length > 0;
|
||||
};
|
||||
Reference in New Issue
Block a user