From d252957dd27ab6831f722a003d641180a7e05717 Mon Sep 17 00:00:00 2001 From: Gemini AI Date: Sun, 14 Dec 2025 20:35:11 +0400 Subject: [PATCH] TUI5: Added 4 new feature modules - Session Memory, Context Manager, Skills Library, Debug Logger --- bin/opencode-ink.mjs | 195 ++++++++++++++++--------------- lib/context-manager.mjs | 181 +++++++++++++++++++++++++++++ lib/debug-logger.mjs | 219 +++++++++++++++++++++++++++++++++++ lib/session-memory.mjs | 176 ++++++++++++++++++++++++++++ lib/skills.mjs | 251 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 930 insertions(+), 92 deletions(-) create mode 100644 lib/context-manager.mjs create mode 100644 lib/debug-logger.mjs create mode 100644 lib/session-memory.mjs create mode 100644 lib/skills.mjs diff --git a/bin/opencode-ink.mjs b/bin/opencode-ink.mjs index c9c955e..7458968 100644 --- a/bin/opencode-ink.mjs +++ b/bin/opencode-ink.mjs @@ -43,6 +43,17 @@ import ThinkingBlock from './ui/components/ThinkingBlock.mjs'; import ChatBubble from './ui/components/ChatBubble.mjs'; import TodoList from './ui/components/TodoList.mjs'; +// ═══════════════════════════════════════════════════════════════ +// NEW FEATURE MODULES - Inspired by Mini-Agent, original implementation +// ═══════════════════════════════════════════════════════════════ +import { getSessionMemory } from '../lib/session-memory.mjs'; +import { getContextManager } from '../lib/context-manager.mjs'; +import { getAllSkills, getSkill, executeSkill, getSkillListDisplay } from '../lib/skills.mjs'; +import { getDebugLogger, initFromArgs } from '../lib/debug-logger.mjs'; + +// Initialize debug logger from CLI args +const debugLogger = initFromArgs(); + const { useState, useCallback, useEffect, useRef, useMemo } = React; // Custom hook for terminal dimensions (replaces ink-use-stdout-dimensions) @@ -1051,8 +1062,8 @@ const SmoothCounter = ({ value }) => { }; // Component: EnhancedTypewriterText - Improved text reveal with batching and adaptive speed -const EnhancedTypewriterText = ({ - children, +const EnhancedTypewriterText = ({ + children, speed = 25, batchSize = 1 // Default to 1 for safety, can be increased for batching }) => { @@ -1084,10 +1095,10 @@ const EnhancedTypewriterText = ({ // Calculate batch size (may be smaller near the end) const remaining = fullText.length - positionRef.current; const currentBatchSize = Math.min(batchSize, remaining); - + // Get the next batch of characters const nextBatch = fullText.substring(positionRef.current, positionRef.current + currentBatchSize); - + // Update display and position setDisplayText(prev => prev + nextBatch); positionRef.current += currentBatchSize; @@ -3362,106 +3373,106 @@ This gives the user a chance to refine requirements before implementation. const fullPrompt = systemPrompt + '\n\n[USER REQUEST]\n' + fullText; let fullResponse = ''; -// PROVIDER SWITCH: Use OpenCode Free or Qwen based on provider state - const streamStartTime = Date.now(); // Track start time for this request - let totalCharsReceived = 0; // Track total characters for speed calculation - - const result = provider === 'opencode-free' - ? await callOpenCodeFree(fullPrompt, freeModel, (chunk) => { - const cleanChunk = chunk.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); + // PROVIDER SWITCH: Use OpenCode Free or Qwen based on provider state + const streamStartTime = Date.now(); // Track start time for this request + let totalCharsReceived = 0; // Track total characters for speed calculation - // IMPROVED STREAM SPLITTING LOGIC (Thinking vs Content) - // Claude Code style: cleaner separation of thinking from response - const lines = cleanChunk.split('\n'); - let isThinkingChunk = false; + const result = provider === 'opencode-free' + ? await callOpenCodeFree(fullPrompt, freeModel, (chunk) => { + const cleanChunk = chunk.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); - // Enhanced heuristics for better Claude-like thinking detection - const trimmedChunk = cleanChunk.trim(); - if (/^(Let me|Now let me|I'll|I need to|I should|I notice|I can|I will|Thinking:|Analyzing|Considering|Checking|Looking|Planning|First|Next|Finally)/i.test(trimmedChunk)) { - isThinkingChunk = true; - } else if (/^```|# |Here is|```|```|```/i.test(trimmedChunk)) { - // If we encounter code blocks or headers, likely content not thinking - isThinkingChunk = false; - } + // IMPROVED STREAM SPLITTING LOGIC (Thinking vs Content) + // Claude Code style: cleaner separation of thinking from response + const lines = cleanChunk.split('\n'); + let isThinkingChunk = false; - // Update character count for speed calculation - totalCharsReceived += cleanChunk.length; - - // Calculate current streaming speed (chars per second) - const elapsedSeconds = (Date.now() - streamStartTime) / 1000; - const speed = elapsedSeconds > 0 ? Math.round(totalCharsReceived / elapsedSeconds) : 0; + // Enhanced heuristics for better Claude-like thinking detection + const trimmedChunk = cleanChunk.trim(); + if (/^(Let me|Now let me|I'll|I need to|I should|I notice|I can|I will|Thinking:|Analyzing|Considering|Checking|Looking|Planning|First|Next|Finally)/i.test(trimmedChunk)) { + isThinkingChunk = true; + } else if (/^```|# |Here is|```|```|```/i.test(trimmedChunk)) { + // If we encounter code blocks or headers, likely content not thinking + isThinkingChunk = false; + } - // GLOBAL STATS UPDATE (Run for ALL chunks) - setThinkingStats(prev => ({ - ...prev, - chars: totalCharsReceived, - speed: speed - })); + // Update character count for speed calculation + totalCharsReceived += cleanChunk.length; - // GLOBAL AGENT DETECTION (Run for ALL chunks) - const agentMatch = cleanChunk.match(/\[AGENT:\s*([^\]]+)\]/i); - if (agentMatch) { - setThinkingStats(prev => ({ ...prev, activeAgent: agentMatch[1].trim() })); - } + // Calculate current streaming speed (chars per second) + const elapsedSeconds = (Date.now() - streamStartTime) / 1000; + const speed = elapsedSeconds > 0 ? Math.round(totalCharsReceived / elapsedSeconds) : 0; - if (isThinkingChunk) { - setThinkingLines(prev => [...prev, ...lines.map(l => l.trim()).filter(l => l && !/^(Let me|Now let me|I'll|I need to|I notice)/i.test(l.trim()))]); - } else { - setMessages(prev => { - const last = prev[prev.length - 1]; - if (last && last.role === 'assistant') { - return [...prev.slice(0, -1), { ...last, content: last.content + cleanChunk }]; - } - return prev; - }); - } - }) -: await getQwen().sendMessage(fullPrompt, 'qwen-coder-plus', null, (chunk) => { - const cleanChunk = chunk.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); + // GLOBAL STATS UPDATE (Run for ALL chunks) + setThinkingStats(prev => ({ + ...prev, + chars: totalCharsReceived, + speed: speed + })); - // IMPROVED STREAM SPLITTING LOGIC (Thinking vs Content) - const lines = cleanChunk.split('\n'); - let isThinkingChunk = false; + // GLOBAL AGENT DETECTION (Run for ALL chunks) + const agentMatch = cleanChunk.match(/\[AGENT:\s*([^\]]+)\]/i); + if (agentMatch) { + setThinkingStats(prev => ({ ...prev, activeAgent: agentMatch[1].trim() })); + } - // Enhanced heuristics for better Claude-like thinking detection - const trimmedChunk = cleanChunk.trim(); - if (/^(Let me|Now let me|I'll|I need to|I should|I notice|I can|I will|Thinking:|Analyzing|Considering|Checking|Looking|Planning|First|Next|Finally)/i.test(trimmedChunk)) { - isThinkingChunk = true; - } else if (/^```|# |Here is|```|```|```/i.test(trimmedChunk)) { - // If we encounter code blocks or headers, likely content not thinking - isThinkingChunk = false; - } + if (isThinkingChunk) { + setThinkingLines(prev => [...prev, ...lines.map(l => l.trim()).filter(l => l && !/^(Let me|Now let me|I'll|I need to|I notice)/i.test(l.trim()))]); + } else { + setMessages(prev => { + const last = prev[prev.length - 1]; + if (last && last.role === 'assistant') { + return [...prev.slice(0, -1), { ...last, content: last.content + cleanChunk }]; + } + return prev; + }); + } + }) + : await getQwen().sendMessage(fullPrompt, 'qwen-coder-plus', null, (chunk) => { + const cleanChunk = chunk.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); - // Update character count for speed calculation (using same variable as OpenCode path) - totalCharsReceived += cleanChunk.length; - - // Calculate current streaming speed (chars per second) - const elapsedSeconds = (Date.now() - streamStartTime) / 1000; - const speed = elapsedSeconds > 0 ? Math.round(totalCharsReceived / elapsedSeconds) : 0; + // IMPROVED STREAM SPLITTING LOGIC (Thinking vs Content) + const lines = cleanChunk.split('\n'); + let isThinkingChunk = false; - setThinkingStats(prev => ({ - ...prev, - chars: totalCharsReceived, - speed: speed - })); + // Enhanced heuristics for better Claude-like thinking detection + const trimmedChunk = cleanChunk.trim(); + if (/^(Let me|Now let me|I'll|I need to|I should|I notice|I can|I will|Thinking:|Analyzing|Considering|Checking|Looking|Planning|First|Next|Finally)/i.test(trimmedChunk)) { + isThinkingChunk = true; + } else if (/^```|# |Here is|```|```|```/i.test(trimmedChunk)) { + // If we encounter code blocks or headers, likely content not thinking + isThinkingChunk = false; + } - const agentMatch = cleanChunk.match(/\[AGENT:\s*([^\]]+)\]/i); - if (agentMatch) { - setThinkingStats(prev => ({ ...prev, activeAgent: agentMatch[1].trim() })); - } + // Update character count for speed calculation (using same variable as OpenCode path) + totalCharsReceived += cleanChunk.length; - if (isThinkingChunk) { - setThinkingLines(prev => [...prev, ...lines.map(l => l.trim()).filter(l => l && !/^(Let me|Now let me|I'll|I need to|I notice)/i.test(l.trim()))]); - } else { - setMessages(prev => { - const last = prev[prev.length - 1]; - if (last && last.role === 'assistant') { - return [...prev.slice(0, -1), { ...last, content: last.content + cleanChunk }]; - } - return prev; - }); - } - }); + // Calculate current streaming speed (chars per second) + const elapsedSeconds = (Date.now() - streamStartTime) / 1000; + const speed = elapsedSeconds > 0 ? Math.round(totalCharsReceived / elapsedSeconds) : 0; + + setThinkingStats(prev => ({ + ...prev, + chars: totalCharsReceived, + speed: speed + })); + + const agentMatch = cleanChunk.match(/\[AGENT:\s*([^\]]+)\]/i); + if (agentMatch) { + setThinkingStats(prev => ({ ...prev, activeAgent: agentMatch[1].trim() })); + } + + if (isThinkingChunk) { + setThinkingLines(prev => [...prev, ...lines.map(l => l.trim()).filter(l => l && !/^(Let me|Now let me|I'll|I need to|I notice)/i.test(l.trim()))]); + } else { + setMessages(prev => { + const last = prev[prev.length - 1]; + if (last && last.role === 'assistant') { + return [...prev.slice(0, -1), { ...last, content: last.content + cleanChunk }]; + } + return prev; + }); + } + }); if (result.success) { const responseText = result.response || fullResponse; diff --git a/lib/context-manager.mjs b/lib/context-manager.mjs new file mode 100644 index 0000000..8fb3ec9 --- /dev/null +++ b/lib/context-manager.mjs @@ -0,0 +1,181 @@ +/** + * Context Manager - Intelligent context window management for TUI 5 + * Auto-summarizes conversation history when approaching token limits + * + * Original implementation for OpenQode TUI + */ + +import { getQwen } from '../qwen-oauth.mjs'; + +// Rough token estimation: ~4 chars per token for English +const CHARS_PER_TOKEN = 4; + +/** + * ContextManager class - Manages conversation context and auto-summarization + */ +export class ContextManager { + constructor(options = {}) { + this.tokenLimit = options.tokenLimit || 100000; // Default 100K token context + this.summarizeThreshold = options.summarizeThreshold || 0.5; // Summarize at 50% + this.minMessagesToKeep = options.minMessagesToKeep || 4; // Keep last 4 messages + this.summaryBlock = null; // Stores summarized context + } + + /** + * Estimate token count for text + * @param {string} text - Text to count + * @returns {number} Estimated token count + */ + countTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / CHARS_PER_TOKEN); + } + + /** + * Count tokens for all messages + * @param {Array} messages - Array of message objects + * @returns {number} Total estimated tokens + */ + countMessageTokens(messages) { + return messages.reduce((total, msg) => { + return total + this.countTokens(msg.content || ''); + }, 0); + } + + /** + * Get current context usage as percentage + * @param {Array} messages - Current messages + * @returns {number} Percentage (0-100) + */ + getUsagePercent(messages) { + const used = this.countMessageTokens(messages); + return Math.round((used / this.tokenLimit) * 100); + } + + /** + * Check if summarization is needed + * @param {Array} messages - Current messages + * @returns {boolean} + */ + shouldSummarize(messages) { + const usage = this.getUsagePercent(messages) / 100; + return usage >= this.summarizeThreshold && messages.length > this.minMessagesToKeep; + } + + /** + * Summarize older messages to free up context + * @param {Array} messages - All messages + * @param {Function} onProgress - Progress callback + * @returns {Object} { summary, keptMessages } + */ + async summarize(messages, onProgress = null) { + if (messages.length <= this.minMessagesToKeep) { + return { summary: null, keptMessages: messages }; + } + + // Split: messages to summarize vs messages to keep + const toSummarize = messages.slice(0, -this.minMessagesToKeep); + const toKeep = messages.slice(-this.minMessagesToKeep); + + if (onProgress) onProgress('Summarizing context...'); + + // Create summary prompt + const summaryPrompt = `Summarize the following conversation history into a concise context summary. +Focus on: +- Key decisions made +- Important context established +- User preferences expressed +- Current project/task state + +Keep it under 500 words. + +CONVERSATION TO SUMMARIZE: +${toSummarize.map(m => `[${m.role}]: ${m.content}`).join('\n\n')} + +SUMMARY:`; + + try { + // Use AI to generate summary + const oauth = getQwen(); + const result = await oauth.sendMessage(summaryPrompt, 'qwen-turbo'); + + if (result.success) { + this.summaryBlock = { + type: 'context_summary', + content: result.response, + originalMessageCount: toSummarize.length, + timestamp: new Date().toISOString() + }; + + return { + summary: this.summaryBlock, + keptMessages: toKeep + }; + } + } catch (error) { + console.error('Context summarization failed:', error.message); + } + + // Fallback: simple truncation + this.summaryBlock = { + type: 'context_truncated', + content: `[Previous ${toSummarize.length} messages truncated to save context]`, + originalMessageCount: toSummarize.length, + timestamp: new Date().toISOString() + }; + + return { + summary: this.summaryBlock, + keptMessages: toKeep + }; + } + + /** + * Get context summary as system prompt addition + */ + getSummaryContext() { + if (!this.summaryBlock) return ''; + + return ` +=== PREVIOUS CONTEXT SUMMARY === +The following is a summary of earlier conversation (${this.summaryBlock.originalMessageCount} messages): + +${this.summaryBlock.content} + +=== END SUMMARY === + +`; + } + + /** + * Get stats for UI display + * @param {Array} messages - Current messages + * @returns {Object} Stats object + */ + getStats(messages) { + const tokens = this.countMessageTokens(messages); + const percent = this.getUsagePercent(messages); + const needsSummary = this.shouldSummarize(messages); + + return { + tokens, + limit: this.tokenLimit, + percent, + needsSummary, + hasSummary: !!this.summaryBlock, + color: percent > 80 ? 'red' : percent > 50 ? 'yellow' : 'green' + }; + } +} + +// Singleton instance +let _contextManager = null; + +export function getContextManager(options = {}) { + if (!_contextManager) { + _contextManager = new ContextManager(options); + } + return _contextManager; +} + +export default ContextManager; diff --git a/lib/debug-logger.mjs b/lib/debug-logger.mjs new file mode 100644 index 0000000..9f94a7b --- /dev/null +++ b/lib/debug-logger.mjs @@ -0,0 +1,219 @@ +/** + * Debug Logger - Comprehensive request/response logging for TUI 5 + * Enabled via --debug flag or /debug command + * + * Original implementation for OpenQode TUI + */ + +import { appendFile, writeFile, readFile, access } from 'fs/promises'; +import { join } from 'path'; + +const DEBUG_FILE = '.openqode-debug.log'; + +/** + * DebugLogger class - Logs all API requests, responses, and tool executions + */ +export class DebugLogger { + constructor(options = {}) { + this.enabled = options.enabled || false; + this.logPath = options.logPath || join(process.cwd(), DEBUG_FILE); + this.maxLogSize = options.maxLogSize || 5 * 1024 * 1024; // 5MB max + this.sessionId = Date.now().toString(36); + } + + /** + * Enable debug logging + */ + enable() { + this.enabled = true; + this.log('DEBUG', 'Debug logging enabled'); + } + + /** + * Disable debug logging + */ + disable() { + this.log('DEBUG', 'Debug logging disabled'); + this.enabled = false; + } + + /** + * Toggle debug mode + */ + toggle() { + if (this.enabled) { + this.disable(); + } else { + this.enable(); + } + return this.enabled; + } + + /** + * Format timestamp + */ + timestamp() { + return new Date().toISOString(); + } + + /** + * Log a message + * @param {string} level - Log level (INFO, DEBUG, WARN, ERROR, API, TOOL) + * @param {string} message - Log message + * @param {Object} data - Optional data to log + */ + async log(level, message, data = null) { + if (!this.enabled && level !== 'DEBUG') return; + + const entry = { + timestamp: this.timestamp(), + session: this.sessionId, + level, + message, + ...(data && { data: this.truncate(data) }) + }; + + const logLine = JSON.stringify(entry) + '\n'; + + try { + await appendFile(this.logPath, logLine); + } catch (error) { + // Silent fail - debug logging shouldn't break the app + } + } + + /** + * Log API request + */ + async logRequest(provider, model, prompt, options = {}) { + await this.log('API_REQUEST', `${provider}/${model}`, { + promptLength: prompt?.length || 0, + promptPreview: prompt?.substring(0, 200) + '...', + options + }); + } + + /** + * Log API response + */ + async logResponse(provider, model, response, duration) { + await this.log('API_RESPONSE', `${provider}/${model}`, { + success: response?.success, + responseLength: response?.response?.length || 0, + responsePreview: response?.response?.substring(0, 200) + '...', + durationMs: duration, + usage: response?.usage + }); + } + + /** + * Log tool execution + */ + async logTool(toolName, input, output, duration) { + await this.log('TOOL', toolName, { + input: this.truncate(input), + output: this.truncate(output), + durationMs: duration + }); + } + + /** + * Log error + */ + async logError(context, error) { + await this.log('ERROR', context, { + message: error?.message, + stack: error?.stack?.substring(0, 500) + }); + } + + /** + * Log user command + */ + async logCommand(command, args) { + await this.log('COMMAND', command, { args }); + } + + /** + * Truncate large objects for logging + */ + truncate(obj, maxLength = 1000) { + if (!obj) return obj; + + if (typeof obj === 'string') { + return obj.length > maxLength + ? obj.substring(0, maxLength) + '...[truncated]' + : obj; + } + + try { + const str = JSON.stringify(obj); + if (str.length > maxLength) { + return JSON.parse(str.substring(0, maxLength) + '..."}}'); + } + return obj; + } catch { + return '[Object]'; + } + } + + /** + * Clear log file + */ + async clear() { + try { + await writeFile(this.logPath, ''); + await this.log('DEBUG', 'Log file cleared'); + } catch (error) { + // Ignore + } + } + + /** + * Get recent log entries + * @param {number} count - Number of entries to return + */ + async getRecent(count = 50) { + try { + const content = await readFile(this.logPath, 'utf8'); + const lines = content.trim().split('\n').filter(l => l); + return lines.slice(-count).map(l => { + try { + return JSON.parse(l); + } catch { + return { raw: l }; + } + }); + } catch { + return []; + } + } + + /** + * Get log file path + */ + getPath() { + return this.logPath; + } +} + +// Singleton instance +let _logger = null; + +export function getDebugLogger(options = {}) { + if (!_logger) { + _logger = new DebugLogger(options); + } + return _logger; +} + +// Check CLI args for --debug flag +export function initFromArgs() { + const logger = getDebugLogger(); + if (process.argv.includes('--debug')) { + logger.enable(); + } + return logger; +} + +export default DebugLogger; diff --git a/lib/session-memory.mjs b/lib/session-memory.mjs new file mode 100644 index 0000000..575c61f --- /dev/null +++ b/lib/session-memory.mjs @@ -0,0 +1,176 @@ +/** + * Session Memory - Persistent context storage for TUI 5 + * Allows AI to remember important facts across sessions + * + * Original implementation for OpenQode TUI + */ + +import { readFile, writeFile, access } from 'fs/promises'; +import { join } from 'path'; +import { homedir } from 'os'; + +const MEMORY_FILE = '.openqode-memory.json'; + +/** + * SessionMemory class - Manages persistent facts/context across TUI sessions + */ +export class SessionMemory { + constructor(projectPath = null) { + this.projectPath = projectPath || process.cwd(); + this.memoryPath = join(this.projectPath, MEMORY_FILE); + this.facts = []; + this.metadata = { + created: null, + lastModified: null, + version: '1.0' + }; + } + + /** + * Load memory from disk + */ + async load() { + try { + await access(this.memoryPath); + const data = await readFile(this.memoryPath, 'utf8'); + const parsed = JSON.parse(data); + this.facts = parsed.facts || []; + this.metadata = parsed.metadata || this.metadata; + return true; + } catch (error) { + // No memory file exists yet - that's OK + this.facts = []; + this.metadata.created = new Date().toISOString(); + return false; + } + } + + /** + * Save memory to disk + */ + async save() { + this.metadata.lastModified = new Date().toISOString(); + if (!this.metadata.created) { + this.metadata.created = this.metadata.lastModified; + } + + const data = { + version: '1.0', + metadata: this.metadata, + facts: this.facts + }; + + await writeFile(this.memoryPath, JSON.stringify(data, null, 2), 'utf8'); + return true; + } + + /** + * Remember a new fact + * @param {string} fact - The fact to remember + * @param {string} category - Optional category (context, decision, preference, etc.) + */ + async remember(fact, category = 'context') { + const entry = { + id: Date.now(), + fact: fact.trim(), + category, + timestamp: new Date().toISOString() + }; + + this.facts.push(entry); + await this.save(); + return entry; + } + + /** + * Forget a fact by ID or index + * @param {number} identifier - Fact ID or index (1-based for user convenience) + */ + async forget(identifier) { + // Try by index first (1-based) + if (identifier > 0 && identifier <= this.facts.length) { + const removed = this.facts.splice(identifier - 1, 1)[0]; + await this.save(); + return removed; + } + + // Try by ID + const index = this.facts.findIndex(f => f.id === identifier); + if (index !== -1) { + const removed = this.facts.splice(index, 1)[0]; + await this.save(); + return removed; + } + + return null; + } + + /** + * Clear all memory + */ + async clear() { + this.facts = []; + await this.save(); + return true; + } + + /** + * Get all facts as a formatted string for AI context + */ + getContextString() { + if (this.facts.length === 0) { + return ''; + } + + const header = '=== SESSION MEMORY ===\nThe following facts were remembered from previous sessions:\n'; + const factsList = this.facts.map((f, i) => + `${i + 1}. [${f.category}] ${f.fact}` + ).join('\n'); + + return header + factsList + '\n=== END MEMORY ===\n\n'; + } + + /** + * Get facts formatted for display in UI + */ + getDisplayList() { + return this.facts.map((f, i) => ({ + index: i + 1, + ...f, + displayDate: new Date(f.timestamp).toLocaleDateString() + })); + } + + /** + * Get memory summary for welcome screen + */ + getSummary() { + const count = this.facts.length; + if (count === 0) { + return 'No session memory stored'; + } + + const categories = {}; + this.facts.forEach(f => { + categories[f.category] = (categories[f.category] || 0) + 1; + }); + + const breakdown = Object.entries(categories) + .map(([cat, num]) => `${num} ${cat}`) + .join(', '); + + return `${count} facts remembered (${breakdown})`; + } +} + +// Singleton instance for easy import +let _memoryInstance = null; + +export function getSessionMemory(projectPath = null) { + if (!_memoryInstance) { + _memoryInstance = new SessionMemory(projectPath); + } + return _memoryInstance; +} + +export default SessionMemory; diff --git a/lib/skills.mjs b/lib/skills.mjs new file mode 100644 index 0000000..1d5abea --- /dev/null +++ b/lib/skills.mjs @@ -0,0 +1,251 @@ +/** + * Skills Library - Pre-built AI prompts for common tasks + * Provides /skills and /skill commands + * + * Original implementation for OpenQode TUI + */ + +/** + * Skill definition structure + */ +const SKILLS = { + // Development Skills + test: { + name: 'Unit Tests', + description: 'Generate comprehensive unit tests for code', + category: 'development', + prompt: `Generate comprehensive unit tests for the provided code. +Include: +- Edge cases and boundary conditions +- Error handling scenarios +- Mock dependencies where appropriate +- Clear test descriptions +- Setup and teardown if needed + +Format: Use the appropriate testing framework for the language (Jest, pytest, etc.)` + }, + + refactor: { + name: 'Refactor Code', + description: 'Suggest refactoring improvements', + category: 'development', + prompt: `Analyze the provided code and suggest refactoring improvements. +Focus on: +- Code clarity and readability +- DRY principle violations +- Performance optimizations +- Design pattern opportunities +- Type safety improvements + +Provide before/after examples for each suggestion.` + }, + + review: { + name: 'Code Review', + description: 'Perform a thorough code review', + category: 'development', + prompt: `Perform a thorough code review of the provided code. +Check for: +- Bugs and logic errors +- Security vulnerabilities +- Performance issues +- Code style and consistency +- Documentation gaps +- Error handling + +Rate severity: 🔴 Critical | 🟡 Warning | 🟢 Suggestion` + }, + + debug: { + name: 'Debug Helper', + description: 'Help diagnose and fix bugs', + category: 'development', + prompt: `Help debug the provided code/error. +Approach: +1. Identify the root cause +2. Explain why the error occurs +3. Provide the fix with explanation +4. Suggest prevention strategies + +Include stack trace analysis if provided.` + }, + + // Documentation Skills + docs: { + name: 'Documentation', + description: 'Generate comprehensive documentation', + category: 'documentation', + prompt: `Generate comprehensive documentation for the provided code. +Include: +- Overview/purpose +- Installation/setup (if applicable) +- API reference with parameters and return values +- Usage examples +- Configuration options +- Common issues/FAQ + +Format: Markdown with proper headings.` + }, + + readme: { + name: 'README Generator', + description: 'Create a professional README.md', + category: 'documentation', + prompt: `Create a professional README.md for this project. +Include: +- Project title and badges +- Description +- Features list +- Quick start guide +- Installation steps +- Usage examples +- Configuration +- Contributing guidelines +- License + +Make it visually appealing with emojis and formatting.` + }, + + // Analysis Skills + explain: { + name: 'Code Explainer', + description: 'Explain code in simple terms', + category: 'analysis', + prompt: `Explain the provided code in simple, clear terms. +Structure: +1. High-level purpose (what it does) +2. Step-by-step walkthrough +3. Key concepts used +4. How it fits in larger context + +Use analogies where helpful. Suitable for juniors.` + }, + + security: { + name: 'Security Audit', + description: 'Check for security vulnerabilities', + category: 'analysis', + prompt: `Perform a security audit of the provided code. +Check for: +- Injection vulnerabilities (SQL, XSS, etc.) +- Authentication/authorization issues +- Sensitive data exposure +- Insecure dependencies +- Cryptographic weaknesses +- OWASP Top 10 issues + +Severity: 🔴 Critical | 🟠 High | 🟡 Medium | 🟢 Low` + }, + + // Generation Skills + api: { + name: 'API Design', + description: 'Design REST API endpoints', + category: 'generation', + prompt: `Design REST API endpoints for the described functionality. +Include: +- Endpoint paths and methods +- Request/response schemas (JSON) +- Status codes +- Authentication requirements +- Rate limiting suggestions +- OpenAPI/Swagger format if helpful` + }, + + schema: { + name: 'Database Schema', + description: 'Design database schema', + category: 'generation', + prompt: `Design a database schema for the described requirements. +Include: +- Tables and columns with types +- Primary/foreign keys +- Indexes for performance +- Relationships diagram (text-based) +- Migration script if helpful + +Consider normalization and query patterns.` + } +}; + +/** + * Get all available skills + */ +export function getAllSkills() { + return Object.entries(SKILLS).map(([id, skill]) => ({ + id, + ...skill + })); +} + +/** + * Get skills grouped by category + */ +export function getSkillsByCategory() { + const categories = {}; + + Object.entries(SKILLS).forEach(([id, skill]) => { + if (!categories[skill.category]) { + categories[skill.category] = []; + } + categories[skill.category].push({ id, ...skill }); + }); + + return categories; +} + +/** + * Get a specific skill by ID + */ +export function getSkill(skillId) { + return SKILLS[skillId] ? { id: skillId, ...SKILLS[skillId] } : null; +} + +/** + * Execute a skill - returns the prompt to inject + * @param {string} skillId - Skill ID + * @param {string} userInput - User's additional input/code + */ +export function executeSkill(skillId, userInput = '') { + const skill = getSkill(skillId); + if (!skill) return null; + + const fullPrompt = `[SKILL: ${skill.name}] + +${skill.prompt} + +USER INPUT/CODE: +${userInput} + +Please proceed with the ${skill.name.toLowerCase()} task.`; + + return { + skill, + prompt: fullPrompt + }; +} + +/** + * Get formatted skill list for display + */ +export function getSkillListDisplay() { + const categories = getSkillsByCategory(); + let output = ''; + + for (const [category, skills] of Object.entries(categories)) { + output += `\n📁 ${category.toUpperCase()}\n`; + skills.forEach(skill => { + output += ` /skill ${skill.id.padEnd(10)} - ${skill.description}\n`; + }); + } + + return output; +} + +export default { + getAllSkills, + getSkillsByCategory, + getSkill, + executeSkill, + getSkillListDisplay +};