bump version to 6.2.6 in package.json

This commit is contained in:
catlog22
2025-12-22 20:17:38 +08:00
parent 6a69af3bf1
commit cf58dc0dd3
17 changed files with 2075 additions and 69 deletions

8
ccw/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ccw",
"version": "6.1.4",
"name": "claude-code-workflow",
"version": "6.2.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ccw",
"version": "6.1.4",
"name": "claude-code-workflow",
"version": "6.2.6",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",

View File

@@ -1,6 +1,6 @@
{
"name": "claude-code-workflow",
"version": "6.2.4",
"version": "6.2.6",
"description": "Claude Code Workflow CLI - Dashboard viewer for workflow sessions and reviews",
"type": "module",
"main": "dist/index.js",

View File

@@ -284,8 +284,12 @@ function normalizeTask(task: unknown): NormalizedTask | null {
const implementation = taskObj.implementation as unknown[] | undefined;
const modificationPoints = taskObj.modification_points as Array<{ file?: string }> | undefined;
// Ensure id is always a string (handle numeric IDs from JSON)
const rawId = taskObj.id ?? taskObj.task_id;
const stringId = rawId != null ? String(rawId) : 'unknown';
return {
id: (taskObj.id as string) || (taskObj.task_id as string) || 'unknown',
id: stringId,
title: (taskObj.title as string) || (taskObj.name as string) || (taskObj.summary as string) || 'Untitled Task',
status: (status as string).toLowerCase(),
// Preserve original fields for flexible rendering

View File

@@ -284,8 +284,12 @@ function normalizeTask(task: unknown): NormalizedTask | null {
const implementation = taskObj.implementation as unknown[] | undefined;
const modificationPoints = taskObj.modification_points as Array<{ file?: string }> | undefined;
// Ensure id is always a string (handle numeric IDs from JSON)
const rawId = taskObj.id ?? taskObj.task_id;
const stringId = rawId != null ? String(rawId) : 'unknown';
return {
id: (taskObj.id as string) || (taskObj.task_id as string) || 'unknown',
id: stringId,
title: (taskObj.title as string) || (taskObj.name as string) || (taskObj.summary as string) || 'Untitled Task',
status: (status as string).toLowerCase(),
// Preserve original fields for flexible rendering

View File

@@ -4,7 +4,7 @@
* Handles all CLAUDE.md memory rules management endpoints
*/
import type { IncomingMessage, ServerResponse } from 'http';
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, unlinkSync, mkdirSync } from 'fs';
import { join, relative } from 'path';
import { homedir } from 'os';
@@ -453,8 +453,7 @@ function deleteClaudeFile(filePath: string): { success: boolean; error?: string
writeFileSync(backupPath, content, 'utf8');
// Delete original file
const fs = require('fs');
fs.unlinkSync(filePath);
unlinkSync(filePath);
return { success: true };
} catch (error) {
@@ -500,9 +499,8 @@ function createNewClaudeFile(level: 'user' | 'project' | 'module', template: str
// Ensure directory exists
const dir = filePath.substring(0, filePath.lastIndexOf('/') || filePath.lastIndexOf('\\'));
const fs = require('fs');
if (!existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
mkdirSync(dir, { recursive: true });
}
// Write file

View File

@@ -362,8 +362,9 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
const status = url.searchParams.get('status') || null;
const category = url.searchParams.get('category') as 'user' | 'internal' | 'insight' | null;
const search = url.searchParams.get('search') || null;
const recursive = url.searchParams.get('recursive') !== 'false';
getHistoryWithNativeInfo(projectPath, { limit, tool, status, category, search })
getHistoryWithNativeInfo(projectPath, { limit, tool, status, category, search, recursive })
.then(history => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(history));

View File

@@ -291,13 +291,14 @@ FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structu
return { error: `Unknown generation type: ${generationType}` };
}
// Execute CLI tool (Gemini) with at least 10 minutes timeout
// Execute CLI tool (Claude) with at least 10 minutes timeout
const result = await executeCliTool({
tool: 'gemini',
tool: 'claude',
prompt,
mode,
cd: workingDir,
timeout: 600000 // 10 minutes
timeout: 600000, // 10 minutes
category: 'internal'
});
if (!result.success) {

View File

@@ -123,6 +123,7 @@ function getSkillsConfig(projectPath) {
result.projectSkills.push({
name: parsed.name || skill.name,
folderName: skill.name, // Actual folder name for API queries
description: parsed.description,
version: parsed.version,
allowedTools: parsed.allowedTools,
@@ -152,6 +153,7 @@ function getSkillsConfig(projectPath) {
result.userSkills.push({
name: parsed.name || skill.name,
folderName: skill.name, // Actual folder name for API queries
description: parsed.description,
version: parsed.version,
allowedTools: parsed.allowedTools,
@@ -197,6 +199,7 @@ function getSkillDetail(skillName, location, projectPath) {
return {
skill: {
name: parsed.name || skillName,
folderName: skillName, // Actual folder name for API queries
description: parsed.description,
version: parsed.version,
allowedTools: parsed.allowedTools,
@@ -390,7 +393,7 @@ async function importSkill(sourcePath, location, projectPath, customName) {
}
/**
* Generate skill via CLI tool (Gemini)
* Generate skill via CLI tool (Claude)
* @param {Object} params - Generation parameters
* @param {string} params.generationType - 'description' or 'template'
* @param {string} params.description - Skill description from user
@@ -455,9 +458,9 @@ REQUIREMENTS:
3. If the skill requires supporting files (e.g., templates, scripts), create them in the skill folder
4. Ensure all files are properly formatted and follow best practices`;
// Execute CLI tool (Gemini) with write mode
// Execute CLI tool (Claude) with write mode
const result = await executeCliTool({
tool: 'gemini',
tool: 'claude',
prompt,
mode: 'write',
cd: baseDir,
@@ -515,8 +518,143 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// API: Get single skill detail
if (pathname.startsWith('/api/skills/') && req.method === 'GET' && !pathname.endsWith('/skills/')) {
// API: List skill directory contents
if (pathname.match(/^\/api\/skills\/[^/]+\/dir$/) && req.method === 'GET') {
const pathParts = pathname.split('/');
const skillName = decodeURIComponent(pathParts[3]);
const subPath = url.searchParams.get('subpath') || '';
const location = url.searchParams.get('location') || 'project';
const projectPathParam = url.searchParams.get('path') || initialPath;
const baseDir = location === 'project'
? join(projectPathParam, '.claude', 'skills')
: join(homedir(), '.claude', 'skills');
const dirPath = subPath
? join(baseDir, skillName, subPath)
: join(baseDir, skillName);
// Security check: ensure path is within skill folder
if (!dirPath.startsWith(join(baseDir, skillName))) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Access denied' }));
return true;
}
if (!existsSync(dirPath)) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Directory not found' }));
return true;
}
try {
const stat = statSync(dirPath);
if (!stat.isDirectory()) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Path is not a directory' }));
return true;
}
const entries = readdirSync(dirPath, { withFileTypes: true });
const files = entries.map(entry => ({
name: entry.name,
isDirectory: entry.isDirectory(),
path: subPath ? `${subPath}/${entry.name}` : entry.name
}));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ files, subPath, skillName }));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (error as Error).message }));
}
return true;
}
// API: Read skill file content
if (pathname.match(/^\/api\/skills\/[^/]+\/file$/) && req.method === 'GET') {
const pathParts = pathname.split('/');
const skillName = decodeURIComponent(pathParts[3]);
const fileName = url.searchParams.get('filename');
const location = url.searchParams.get('location') || 'project';
const projectPathParam = url.searchParams.get('path') || initialPath;
if (!fileName) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'filename parameter is required' }));
return true;
}
const baseDir = location === 'project'
? join(projectPathParam, '.claude', 'skills')
: join(homedir(), '.claude', 'skills');
const filePath = join(baseDir, skillName, fileName);
// Security check: ensure file is within skill folder
if (!filePath.startsWith(join(baseDir, skillName))) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Access denied' }));
return true;
}
if (!existsSync(filePath)) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'File not found' }));
return true;
}
try {
const content = readFileSync(filePath, 'utf8');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ content, fileName, path: filePath }));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (error as Error).message }));
}
return true;
}
// API: Write skill file content
if (pathname.match(/^\/api\/skills\/[^/]+\/file$/) && req.method === 'POST') {
const pathParts = pathname.split('/');
const skillName = decodeURIComponent(pathParts[3]);
handlePostRequest(req, res, async (body) => {
const { fileName, content, location, projectPath: projectPathParam } = body;
if (!fileName) {
return { error: 'fileName is required' };
}
if (content === undefined) {
return { error: 'content is required' };
}
const baseDir = location === 'project'
? join(projectPathParam || initialPath, '.claude', 'skills')
: join(homedir(), '.claude', 'skills');
const filePath = join(baseDir, skillName, fileName);
// Security check: ensure file is within skill folder
if (!filePath.startsWith(join(baseDir, skillName))) {
return { error: 'Access denied' };
}
try {
await fsPromises.writeFile(filePath, content, 'utf8');
return { success: true, fileName, path: filePath };
} catch (error) {
return { error: (error as Error).message };
}
});
return true;
}
// API: Get single skill detail (exclude /dir and /file sub-routes)
if (pathname.startsWith('/api/skills/') && req.method === 'GET' &&
!pathname.endsWith('/skills/') && !pathname.endsWith('/dir') && !pathname.endsWith('/file')) {
const skillName = decodeURIComponent(pathname.replace('/api/skills/', ''));
const location = url.searchParams.get('location') || 'project';
const projectPathParam = url.searchParams.get('path') || initialPath;
@@ -576,7 +714,7 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise<boolean> {
return await importSkill(sourcePath, location, projectPath, skillName);
} else if (mode === 'cli-generate') {
// CLI generate mode: use Gemini to generate skill
// CLI generate mode: use Claude to generate skill
if (!skillName) {
return { error: 'Skill name is required for CLI generation mode' };
}

View File

@@ -411,9 +411,12 @@ const i18n = {
'index.fullDesc': 'FTS + Semantic search (recommended)',
'index.selectModel': 'Select embedding model',
'index.modelCode': 'Code (768d)',
'index.modelBase': 'Base (768d)',
'index.modelFast': 'Fast (384d)',
'index.modelMultilingual': 'Multilingual (1024d)',
'index.modelBalanced': 'Balanced (1024d)',
'index.modelMinilm': 'MiniLM (384d)',
'index.modelMultilingual': 'Multilingual (1024d) ⚠️',
'index.modelBalanced': 'Balanced (1024d) ⚠️',
'index.dimensionWarning': '1024d models require more resources',
// Semantic Search Configuration
'semantic.settings': 'Semantic Search Settings',
@@ -1824,9 +1827,12 @@ const i18n = {
'index.fullDesc': 'FTS + 语义搜索(推荐)',
'index.selectModel': '选择嵌入模型',
'index.modelCode': '代码优化 (768维)',
'index.modelBase': '通用基础 (768维)',
'index.modelFast': '快速轻量 (384维)',
'index.modelMultilingual': '多语言 (1024维)',
'index.modelBalanced': '高精度 (1024维)',
'index.modelMinilm': 'MiniLM (384维)',
'index.modelMultilingual': '多语言 (1024维) ⚠️',
'index.modelBalanced': '高精度 (1024维) ⚠️',
'index.dimensionWarning': '1024维模型需要更多资源',
// Semantic Search 配置
'semantic.settings': '语义搜索设置',

View File

@@ -102,6 +102,7 @@ async function loadClaudeFiles() {
updateClaudeBadge(); // Update navigation badge
} catch (error) {
console.error('Error loading CLAUDE.md files:', error);
showRefreshToast(t('claudeManager.loadError') || 'Failed to load files', 'error');
addGlobalNotification('error', t('claudeManager.loadError'), null, 'CLAUDE.md');
}
}
@@ -113,6 +114,7 @@ async function refreshClaudeFiles() {
renderFileViewer();
renderFileMetadata();
if (window.lucide) lucide.createIcons();
showRefreshToast(t('claudeManager.refreshed') || 'Files refreshed', 'success');
addGlobalNotification('success', t('claudeManager.refreshed'), null, 'CLAUDE.md');
// Load freshness data in background
loadFreshnessDataAsync();
@@ -155,6 +157,7 @@ async function markFileAsUpdated() {
if (!res.ok) throw new Error('Failed to mark file as updated');
showRefreshToast(t('claudeManager.markedAsUpdated') || 'Marked as updated', 'success');
addGlobalNotification('success', t('claudeManager.markedAsUpdated') || 'Marked as updated', null, 'CLAUDE.md');
// Reload freshness data
@@ -163,6 +166,7 @@ async function markFileAsUpdated() {
renderFileMetadata();
} catch (error) {
console.error('Error marking file as updated:', error);
showRefreshToast(t('claudeManager.markUpdateError') || 'Failed to mark as updated', 'error');
addGlobalNotification('error', t('claudeManager.markUpdateError') || 'Failed to mark as updated', null, 'CLAUDE.md');
}
}
@@ -481,10 +485,12 @@ async function saveClaudeFile() {
selectedFile.stats = calculateFileStats(newContent);
isDirty = false;
showRefreshToast(t('claudeManager.saved') || 'File saved', 'success');
addGlobalNotification('success', t('claudeManager.saved'), null, 'CLAUDE.md');
renderFileMetadata();
} catch (error) {
console.error('Error saving file:', error);
showRefreshToast(t('claudeManager.saveError') || 'Save failed', 'error');
addGlobalNotification('error', t('claudeManager.saveError'), null, 'CLAUDE.md');
}
}
@@ -733,12 +739,13 @@ async function loadFileContent(filePath) {
}
function showClaudeNotification(type, message) {
// Use global notification system if available
// Show toast for immediate feedback
if (typeof showRefreshToast === 'function') {
showRefreshToast(message, type);
}
// Also add to global notification system if available
if (typeof addGlobalNotification === 'function') {
addGlobalNotification(type, message, null, 'CLAUDE.md');
} else {
// Fallback to simple alert
alert(message);
}
}
@@ -822,6 +829,7 @@ async function createNewFile() {
var modulePath = document.getElementById('modulePath').value;
if (level === 'module' && !modulePath) {
showRefreshToast(t('claude.modulePathRequired') || 'Module path is required', 'error');
addGlobalNotification('error', t('claude.modulePathRequired') || 'Module path is required', null, 'CLAUDE.md');
return;
}
@@ -841,12 +849,14 @@ async function createNewFile() {
var result = await res.json();
closeCreateDialog();
showRefreshToast(t('claude.fileCreated') || 'File created successfully', 'success');
addGlobalNotification('success', t('claude.fileCreated') || 'File created successfully', null, 'CLAUDE.md');
// Refresh file tree
await refreshClaudeFiles();
} catch (error) {
console.error('Error creating file:', error);
showRefreshToast(t('claude.createFileError') || 'Failed to create file', 'error');
addGlobalNotification('error', t('claude.createFileError') || 'Failed to create file', null, 'CLAUDE.md');
}
}
@@ -870,6 +880,7 @@ async function confirmDeleteFile() {
if (!res.ok) throw new Error('Failed to delete file');
showRefreshToast(t('claude.fileDeleted') || 'File deleted successfully', 'success');
addGlobalNotification('success', t('claude.fileDeleted') || 'File deleted successfully', null, 'CLAUDE.md');
selectedFile = null;
@@ -877,6 +888,7 @@ async function confirmDeleteFile() {
await refreshClaudeFiles();
} catch (error) {
console.error('Error deleting file:', error);
showRefreshToast(t('claude.deleteFileError') || 'Failed to delete file', 'error');
addGlobalNotification('error', t('claude.deleteFileError') || 'Failed to delete file', null, 'CLAUDE.md');
}
}
@@ -886,9 +898,11 @@ function copyFileContent() {
if (!selectedFile || !selectedFile.content) return;
navigator.clipboard.writeText(selectedFile.content).then(function() {
showRefreshToast(t('claude.contentCopied') || 'Content copied to clipboard', 'success');
addGlobalNotification('success', t('claude.contentCopied') || 'Content copied to clipboard', null, 'CLAUDE.md');
}).catch(function(error) {
console.error('Error copying content:', error);
showRefreshToast(t('claude.copyError') || 'Failed to copy content', 'error');
addGlobalNotification('error', t('claude.copyError') || 'Failed to copy content', null, 'CLAUDE.md');
});
}

View File

@@ -404,10 +404,12 @@ function renderToolsSection() {
(codexLensStatus.ready
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
'<select id="codexlensModelSelect" class="btn-sm bg-muted border border-border rounded text-xs" onclick="event.stopPropagation()" title="' + (t('index.selectModel') || 'Select embedding model') + '">' +
'<option value="code">' + (t('index.modelCode') || 'Code (768d)') + '</option>' +
'<option value="code">' + (t('index.modelCode') || 'Code (768d)') + '</option>' +
'<option value="base">' + (t('index.modelBase') || 'Base (768d)') + '</option>' +
'<option value="fast">' + (t('index.modelFast') || 'Fast (384d)') + '</option>' +
'<option value="multilingual">' + (t('index.modelMultilingual') || 'Multilingual (1024d)') + '</option>' +
'<option value="balanced">' + (t('index.modelBalanced') || 'Balanced (1024d)') + '</option>' +
'<option value="minilm">' + (t('index.modelMinilm') || 'MiniLM (384d)') + '</option>' +
'<option value="multilingual" style="color: var(--muted-foreground)">' + (t('index.modelMultilingual') || 'Multilingual (1024d)') + ' ⚠️</option>' +
'<option value="balanced" style="color: var(--muted-foreground)">' + (t('index.modelBalanced') || 'Balanced (1024d)') + ' ⚠️</option>' +
'</select>' +
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); initCodexLensIndex(\'full\', getSelectedModel())" title="' + (t('index.fullDesc') || 'FTS + Semantic search (recommended)') + '"><i data-lucide="layers" class="w-3 h-3"></i> ' + (t('index.fullIndex') || '全部索引') + '</button>' +
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex(\'vector\', getSelectedModel())" title="' + (t('index.vectorDesc') || 'Semantic search with embeddings') + '"><i data-lucide="sparkles" class="w-3 h-3"></i> ' + (t('index.vectorIndex') || '向量索引') + '</button>' +

View File

@@ -638,9 +638,26 @@ function addRulePath() {
function removeRulePath(index) {
ruleCreateState.paths.splice(index, 1);
// Re-render paths list
closeRuleCreateModal();
openRuleCreateModal();
// Re-render paths list without closing modal
const pathsList = document.getElementById('rulePathsList');
if (pathsList) {
pathsList.innerHTML = ruleCreateState.paths.map((path, idx) => `
<div class="flex gap-2">
<input type="text" class="rule-path-input flex-1 px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="src/**/*.ts"
value="${path}"
data-index="${idx}">
${idx > 0 ? `
<button class="px-3 py-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
onclick="removeRulePath(${idx})">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
` : ''}
</div>
`).join('');
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
function switchRuleCreateMode(mode) {
@@ -674,9 +691,21 @@ function switchRuleCreateMode(mode) {
if (contentSection) contentSection.style.display = 'block';
}
// Re-render modal to update button states
closeRuleCreateModal();
openRuleCreateModal();
// Update mode button styles without re-rendering
const modeButtons = document.querySelectorAll('#ruleCreateModal .mode-btn');
modeButtons.forEach(btn => {
const btnText = btn.querySelector('.font-medium')?.textContent || '';
const isInput = btnText.includes(t('rules.manualInput'));
const isCliGenerate = btnText.includes(t('rules.cliGenerate'));
if ((isInput && mode === 'input') || (isCliGenerate && mode === 'cli-generate')) {
btn.classList.remove('border-border', 'hover:border-primary/50');
btn.classList.add('border-primary', 'bg-primary/10');
} else {
btn.classList.remove('border-primary', 'bg-primary/10');
btn.classList.add('border-border', 'hover:border-primary/50');
}
});
}
function switchRuleGenerationType(type) {

View File

@@ -153,10 +153,11 @@ function renderSkillCard(skill, location) {
const locationIcon = location === 'project' ? 'folder' : 'user';
const locationClass = location === 'project' ? 'text-primary' : 'text-indigo';
const locationBg = location === 'project' ? 'bg-primary/10' : 'bg-indigo/10';
const folderName = skill.folderName || skill.name;
return `
<div class="skill-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all cursor-pointer"
onclick="showSkillDetail('${escapeHtml(skill.name)}', '${location}')">
onclick="showSkillDetail('${escapeHtml(folderName)}', '${location}')">
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-3">
<div class="w-10 h-10 ${locationBg} rounded-lg flex items-center justify-center">
@@ -198,6 +199,7 @@ function renderSkillCard(skill, location) {
function renderSkillDetailPanel(skill) {
const hasAllowedTools = skill.allowedTools && skill.allowedTools.length > 0;
const hasSupportingFiles = skill.supportingFiles && skill.supportingFiles.length > 0;
const folderName = skill.folderName || skill.name;
return `
<div class="skill-detail-panel fixed top-0 right-0 w-1/2 max-w-xl h-full bg-card border-l border-border shadow-lg z-50 flex flex-col">
@@ -243,20 +245,54 @@ function renderSkillDetailPanel(skill) {
</div>
` : ''}
<!-- Supporting Files -->
${hasSupportingFiles ? `
<div>
<h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.supportingFiles')}</h4>
<div class="space-y-2">
${skill.supportingFiles.map(file => `
<div class="flex items-center gap-2 p-2 bg-muted/50 rounded-lg">
<i data-lucide="file-text" class="w-4 h-4 text-muted-foreground"></i>
<span class="text-sm font-mono text-foreground">${escapeHtml(file)}</span>
</div>
`).join('')}
<!-- Skill Files (SKILL.md + Supporting Files) -->
<div>
<h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.files') || 'Files'}</h4>
<div class="space-y-2">
<!-- SKILL.md (main file) -->
<div class="flex items-center justify-between p-2 bg-primary/5 border border-primary/20 rounded-lg cursor-pointer hover:bg-primary/10 transition-colors"
onclick="viewSkillFile('${escapeHtml(folderName)}', 'SKILL.md', '${skill.location}')">
<div class="flex items-center gap-2">
<i data-lucide="file-text" class="w-4 h-4 text-primary"></i>
<span class="text-sm font-mono text-foreground font-medium">SKILL.md</span>
</div>
<div class="flex items-center gap-1">
<button class="p-1 text-primary hover:bg-primary/20 rounded transition-colors"
onclick="event.stopPropagation(); editSkillFile('${escapeHtml(folderName)}', 'SKILL.md', '${skill.location}')"
title="${t('common.edit')}">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
${hasSupportingFiles ? skill.supportingFiles.map(file => {
const isDir = file.endsWith('/');
const dirName = isDir ? file.slice(0, -1) : file;
return `
<!-- Supporting file: ${escapeHtml(file)} -->
<div class="skill-file-item" data-path="${escapeHtml(dirName)}">
<div class="flex items-center justify-between p-2 bg-muted/50 rounded-lg cursor-pointer hover:bg-muted transition-colors"
onclick="${isDir ? `toggleSkillFolder('${escapeHtml(folderName)}', '${escapeHtml(dirName)}', '${skill.location}', this)` : `viewSkillFile('${escapeHtml(folderName)}', '${escapeHtml(file)}', '${skill.location}')`}">
<div class="flex items-center gap-2">
<i data-lucide="${isDir ? 'folder' : 'file-text'}" class="w-4 h-4 text-muted-foreground ${isDir ? 'folder-icon' : ''}"></i>
<span class="text-sm font-mono text-foreground">${escapeHtml(isDir ? dirName : file)}</span>
${isDir ? '<i data-lucide="chevron-right" class="w-3 h-3 text-muted-foreground folder-chevron transition-transform"></i>' : ''}
</div>
${!isDir ? `
<div class="flex items-center gap-1">
<button class="p-1 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors"
onclick="event.stopPropagation(); editSkillFile('${escapeHtml(folderName)}', '${escapeHtml(file)}', '${skill.location}')"
title="${t('common.edit')}">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
</button>
</div>
` : ''}
</div>
<div class="folder-contents hidden ml-4 mt-1 space-y-1"></div>
</div>
`;
}).join('') : ''}
</div>
` : ''}
</div>
<!-- Path -->
<div>
@@ -269,12 +305,12 @@ function renderSkillDetailPanel(skill) {
<!-- Actions -->
<div class="px-5 py-4 border-t border-border flex justify-between">
<button class="px-4 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-lg transition-colors flex items-center gap-2"
onclick="deleteSkill('${escapeHtml(skill.name)}', '${skill.location}')">
onclick="deleteSkill('${escapeHtml(folderName)}', '${skill.location}')">
<i data-lucide="trash-2" class="w-4 h-4"></i>
${t('common.delete')}
</button>
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
onclick="editSkill('${escapeHtml(skill.name)}', '${skill.location}')">
onclick="editSkill('${escapeHtml(folderName)}', '${skill.location}')">
<i data-lucide="edit" class="w-4 h-4"></i>
${t('common.edit')}
</button>
@@ -525,7 +561,7 @@ function openSkillCreateModal() {
</div>
<!-- Footer -->
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
<div id="skillModalFooter" class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
onclick="closeSkillCreateModal()">
${t('common.cancel')}
@@ -588,16 +624,76 @@ function selectSkillLocation(location) {
function switchSkillCreateMode(mode) {
skillCreateState.mode = mode;
// Re-render modal
closeSkillCreateModal();
openSkillCreateModal();
// Toggle visibility of mode sections
const importSection = document.getElementById('skillImportMode');
const cliGenerateSection = document.getElementById('skillCliGenerateMode');
const footerContainer = document.getElementById('skillModalFooter');
if (importSection) importSection.style.display = mode === 'import' ? 'block' : 'none';
if (cliGenerateSection) cliGenerateSection.style.display = mode === 'cli-generate' ? 'block' : 'none';
// Update mode button styles
const modeButtons = document.querySelectorAll('#skillCreateModal .mode-btn');
modeButtons.forEach(btn => {
const btnText = btn.querySelector('.font-medium')?.textContent || '';
const isImport = btnText.includes(t('skills.importFolder'));
const isCliGenerate = btnText.includes(t('skills.cliGenerate'));
if ((isImport && mode === 'import') || (isCliGenerate && mode === 'cli-generate')) {
btn.classList.remove('border-border', 'hover:border-primary/50');
btn.classList.add('border-primary', 'bg-primary/10');
} else {
btn.classList.remove('border-primary', 'bg-primary/10');
btn.classList.add('border-border', 'hover:border-primary/50');
}
});
// Update footer buttons
if (footerContainer) {
if (mode === 'import') {
footerContainer.innerHTML = `
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
onclick="closeSkillCreateModal()">
${t('common.cancel')}
</button>
<button class="px-4 py-2 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
onclick="validateSkillImport()">
${t('skills.validate')}
</button>
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
onclick="createSkill()">
${t('skills.import')}
</button>
`;
} else {
footerContainer.innerHTML = `
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
onclick="closeSkillCreateModal()">
${t('common.cancel')}
</button>
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
onclick="createSkill()">
<i data-lucide="sparkles" class="w-4 h-4"></i>
${t('skills.generate')}
</button>
`;
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
function switchSkillGenerationType(type) {
skillCreateState.generationType = type;
// Re-render modal
closeSkillCreateModal();
openSkillCreateModal();
// Toggle visibility of description area
const descriptionArea = document.getElementById('skillDescriptionArea');
if (descriptionArea) {
descriptionArea.style.display = type === 'description' ? 'block' : 'none';
}
// Update generation type button styles (only the description button is active, template is disabled)
// No need to update button styles since template button is disabled
}
function browseSkillFolder() {
@@ -817,3 +913,271 @@ async function createSkill() {
}
}
}
// ========== Skill File View/Edit Functions ==========
var skillFileEditorState = {
skillName: '',
fileName: '',
location: '',
content: '',
isEditing: false
};
async function viewSkillFile(skillName, fileName, location) {
try {
const response = await fetch(
'/api/skills/' + encodeURIComponent(skillName) + '/file?filename=' + encodeURIComponent(fileName) +
'&location=' + location + '&path=' + encodeURIComponent(projectPath)
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to load file');
}
const data = await response.json();
skillFileEditorState = {
skillName,
fileName,
location,
content: data.content,
isEditing: false
};
renderSkillFileModal();
} catch (err) {
console.error('Failed to load skill file:', err);
if (window.showToast) {
showToast(err.message || t('skills.fileLoadError') || 'Failed to load file', 'error');
}
}
}
function editSkillFile(skillName, fileName, location) {
viewSkillFile(skillName, fileName, location).then(() => {
skillFileEditorState.isEditing = true;
renderSkillFileModal();
});
}
function renderSkillFileModal() {
// Remove existing modal if any
const existingModal = document.getElementById('skillFileModal');
if (existingModal) existingModal.remove();
const { skillName, fileName, content, isEditing, location } = skillFileEditorState;
const modalHtml = `
<div class="modal-overlay fixed inset-0 bg-black/50 z-[60] flex items-center justify-center" onclick="closeSkillFileModal(event)">
<div class="modal-dialog bg-card rounded-lg shadow-lg w-full max-w-4xl max-h-[90vh] mx-4 flex flex-col" onclick="event.stopPropagation()">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-border">
<div class="flex items-center gap-3">
<i data-lucide="file-text" class="w-5 h-5 text-primary"></i>
<div>
<h3 class="text-lg font-semibold text-foreground font-mono">${escapeHtml(fileName)}</h3>
<p class="text-xs text-muted-foreground">${escapeHtml(skillName)} / ${location}</p>
</div>
</div>
<div class="flex items-center gap-2">
${!isEditing ? `
<button class="px-3 py-1.5 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors flex items-center gap-1"
onclick="toggleSkillFileEdit()">
<i data-lucide="edit-2" class="w-4 h-4"></i>
${t('common.edit')}
</button>
` : ''}
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded"
onclick="closeSkillFileModal()">&times;</button>
</div>
</div>
<!-- Content -->
<div class="flex-1 overflow-hidden p-4">
${isEditing ? `
<textarea id="skillFileContent"
class="w-full h-full min-h-[400px] px-4 py-3 bg-background border border-border rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary resize-none"
spellcheck="false">${escapeHtml(content)}</textarea>
` : `
<div class="w-full h-full min-h-[400px] overflow-auto">
<pre class="px-4 py-3 bg-muted/30 rounded-lg text-sm font-mono whitespace-pre-wrap break-words">${escapeHtml(content)}</pre>
</div>
`}
</div>
<!-- Footer -->
${isEditing ? `
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
onclick="cancelSkillFileEdit()">
${t('common.cancel')}
</button>
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
onclick="saveSkillFile()">
<i data-lucide="save" class="w-4 h-4"></i>
${t('common.save')}
</button>
</div>
` : ''}
</div>
</div>
`;
const modalContainer = document.createElement('div');
modalContainer.id = 'skillFileModal';
modalContainer.innerHTML = modalHtml;
document.body.appendChild(modalContainer);
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closeSkillFileModal(event) {
if (event && event.target !== event.currentTarget) return;
const modal = document.getElementById('skillFileModal');
if (modal) modal.remove();
skillFileEditorState = { skillName: '', fileName: '', location: '', content: '', isEditing: false };
}
function toggleSkillFileEdit() {
skillFileEditorState.isEditing = true;
renderSkillFileModal();
}
function cancelSkillFileEdit() {
skillFileEditorState.isEditing = false;
renderSkillFileModal();
}
async function saveSkillFile() {
const contentTextarea = document.getElementById('skillFileContent');
if (!contentTextarea) return;
const newContent = contentTextarea.value;
const { skillName, fileName, location } = skillFileEditorState;
try {
const response = await fetch('/api/skills/' + encodeURIComponent(skillName) + '/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName,
content: newContent,
location,
projectPath
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save file');
}
// Update state and close edit mode
skillFileEditorState.content = newContent;
skillFileEditorState.isEditing = false;
renderSkillFileModal();
// Refresh skill detail if SKILL.md was edited
if (fileName === 'SKILL.md') {
await loadSkillsData();
// Reload current skill detail
if (selectedSkill) {
await showSkillDetail(skillName, location);
}
}
if (window.showToast) {
showToast(t('skills.fileSaved') || 'File saved successfully', 'success');
}
} catch (err) {
console.error('Failed to save skill file:', err);
if (window.showToast) {
showToast(err.message || t('skills.fileSaveError') || 'Failed to save file', 'error');
}
}
}
// ========== Skill Folder Expansion Functions ==========
var expandedFolders = new Set();
async function toggleSkillFolder(skillName, subPath, location, element) {
const fileItem = element.closest('.skill-file-item');
if (!fileItem) return;
const contentsDiv = fileItem.querySelector('.folder-contents');
const chevron = element.querySelector('.folder-chevron');
const folderIcon = element.querySelector('.folder-icon');
const folderKey = `${skillName}:${subPath}:${location}`;
if (expandedFolders.has(folderKey)) {
// Collapse folder
expandedFolders.delete(folderKey);
contentsDiv.classList.add('hidden');
contentsDiv.innerHTML = '';
if (chevron) chevron.style.transform = '';
if (folderIcon) folderIcon.setAttribute('data-lucide', 'folder');
if (typeof lucide !== 'undefined') lucide.createIcons();
} else {
// Expand folder
try {
const response = await fetch(
'/api/skills/' + encodeURIComponent(skillName) + '/dir?subpath=' + encodeURIComponent(subPath) +
'&location=' + location + '&path=' + encodeURIComponent(projectPath)
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to load folder');
}
const data = await response.json();
expandedFolders.add(folderKey);
if (chevron) chevron.style.transform = 'rotate(90deg)';
if (folderIcon) folderIcon.setAttribute('data-lucide', 'folder-open');
// Render folder contents
contentsDiv.innerHTML = data.files.map(file => {
const filePath = file.path;
const isDir = file.isDirectory;
return `
<div class="skill-file-item" data-path="${escapeHtml(filePath)}">
<div class="flex items-center justify-between p-2 bg-muted/30 rounded-lg cursor-pointer hover:bg-muted/50 transition-colors"
onclick="${isDir ? `toggleSkillFolder('${escapeHtml(skillName)}', '${escapeHtml(filePath)}', '${location}', this)` : `viewSkillFile('${escapeHtml(skillName)}', '${escapeHtml(filePath)}', '${location}')`}">
<div class="flex items-center gap-2">
<i data-lucide="${isDir ? 'folder' : 'file-text'}" class="w-4 h-4 text-muted-foreground ${isDir ? 'folder-icon' : ''}"></i>
<span class="text-sm font-mono text-foreground">${escapeHtml(file.name)}</span>
${isDir ? '<i data-lucide="chevron-right" class="w-3 h-3 text-muted-foreground folder-chevron transition-transform"></i>' : ''}
</div>
${!isDir ? `
<div class="flex items-center gap-1">
<button class="p-1 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors"
onclick="event.stopPropagation(); editSkillFile('${escapeHtml(skillName)}', '${escapeHtml(filePath)}', '${location}')"
title="${t('common.edit')}">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
</button>
</div>
` : ''}
</div>
<div class="folder-contents hidden ml-4 mt-1 space-y-1"></div>
</div>
`;
}).join('');
contentsDiv.classList.remove('hidden');
if (typeof lucide !== 'undefined') lucide.createIcons();
} catch (err) {
console.error('Failed to load folder contents:', err);
if (window.showToast) {
showToast(err.message || 'Failed to load folder', 'error');
}
}
}
}

View File

@@ -5,6 +5,7 @@
import { z } from 'zod';
import type { ToolSchema, ToolResult } from '../types/tool.js';
import type { HistoryIndexEntry } from './cli-history-store.js';
import { spawn, ChildProcess } from 'child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync, statSync } from 'fs';
import { join, relative } from 'path';
@@ -1982,6 +1983,7 @@ export async function getEnrichedConversation(baseDir: string, ccwId: string) {
/**
* Get history with native session info
* Supports recursive querying of child projects
*/
export async function getHistoryWithNativeInfo(baseDir: string, options?: {
limit?: number;
@@ -1990,9 +1992,75 @@ export async function getHistoryWithNativeInfo(baseDir: string, options?: {
status?: string | null;
category?: ExecutionCategory | null;
search?: string | null;
recursive?: boolean;
}) {
const store = await getSqliteStore(baseDir);
return store.getHistoryWithNativeInfo(options || {});
const { limit = 50, recursive = false, ...queryOptions } = options || {};
// Non-recursive mode: query single project
if (!recursive) {
const store = await getSqliteStore(baseDir);
return store.getHistoryWithNativeInfo({ limit, ...queryOptions });
}
// Recursive mode: aggregate data from parent and all child projects
const { scanChildProjectsAsync } = await import('../config/storage-paths.js');
const childProjects = await scanChildProjectsAsync(baseDir);
// Use the same type as store.getHistoryWithNativeInfo returns
type ExecutionWithNativeAndSource = HistoryIndexEntry & {
hasNativeSession: boolean;
nativeSessionId?: string;
nativeSessionPath?: string;
};
const allExecutions: ExecutionWithNativeAndSource[] = [];
let totalCount = 0;
// Query parent project
try {
const parentStore = await getSqliteStore(baseDir);
const parentResult = parentStore.getHistoryWithNativeInfo({ limit, ...queryOptions });
totalCount += parentResult.total;
for (const exec of parentResult.executions) {
allExecutions.push({ ...exec, sourceDir: baseDir });
}
} catch (error) {
if (process.env.DEBUG) {
console.error(`[CLI History] Failed to query parent project ${baseDir}:`, error);
}
}
// Query all child projects
for (const child of childProjects) {
try {
const childStore = await getSqliteStore(child.projectPath);
const childResult = childStore.getHistoryWithNativeInfo({ limit, ...queryOptions });
totalCount += childResult.total;
for (const exec of childResult.executions) {
allExecutions.push({ ...exec, sourceDir: child.projectPath });
}
} catch (error) {
if (process.env.DEBUG) {
console.error(`[CLI History] Failed to query child project ${child.projectPath}:`, error);
}
}
}
// Sort by updated_at descending and apply limit
allExecutions.sort((a, b) => {
const timeA = a.updated_at ? new Date(a.updated_at).getTime() : new Date(a.timestamp).getTime();
const timeB = b.updated_at ? new Date(b.updated_at).getTime() : new Date(b.timestamp).getTime();
return timeB - timeA;
});
const limitedExecutions = allExecutions.slice(0, limit);
return {
total: totalCount,
count: limitedExecutions.length,
executions: limitedExecutions
};
}
// Export types

View File

@@ -14,6 +14,8 @@ except ImportError:
# Model profiles with metadata
# Note: 768d is max recommended dimension for optimal performance/quality balance
# 1024d models are available but not recommended due to higher resource usage
MODEL_PROFILES = {
"fast": {
"model_name": "BAAI/bge-small-en-v1.5",
@@ -21,6 +23,15 @@ MODEL_PROFILES = {
"size_mb": 80,
"description": "Fast, lightweight, English-optimized",
"use_case": "Quick prototyping, resource-constrained environments",
"recommended": True,
},
"base": {
"model_name": "BAAI/bge-base-en-v1.5",
"dimensions": 768,
"size_mb": 220,
"description": "General purpose, good balance of speed and quality",
"use_case": "General text search, documentation",
"recommended": True,
},
"code": {
"model_name": "jinaai/jina-embeddings-v2-base-code",
@@ -28,20 +39,31 @@ MODEL_PROFILES = {
"size_mb": 150,
"description": "Code-optimized, best for programming languages",
"use_case": "Open source projects, code semantic search",
"recommended": True,
},
"minilm": {
"model_name": "sentence-transformers/all-MiniLM-L6-v2",
"dimensions": 384,
"size_mb": 90,
"description": "Popular lightweight model, good quality",
"use_case": "General purpose, low resource environments",
"recommended": True,
},
"multilingual": {
"model_name": "intfloat/multilingual-e5-large",
"dimensions": 1024,
"size_mb": 1000,
"description": "Multilingual + code support",
"description": "Multilingual + code support (high resource usage)",
"use_case": "Enterprise multilingual projects",
"recommended": False, # 1024d not recommended
},
"balanced": {
"model_name": "mixedbread-ai/mxbai-embed-large-v1",
"dimensions": 1024,
"size_mb": 600,
"description": "High accuracy, general purpose",
"description": "High accuracy, general purpose (high resource usage)",
"use_case": "High-quality semantic search, balanced performance",
"recommended": False, # 1024d not recommended
},
}

1361
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "claude-code-workflow",
"version": "6.2.4",
"version": "6.2.6",
"description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",
"type": "module",
"main": "ccw/src/index.js",