mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
- Fixed API endpoint mismatches in mcp-manager.js to ensure global install/update buttons function correctly. - Corrected undefined function references in mcp-manager.js for project installation. - Refactored event handling to eliminate global scope pollution in mcp-manager.js. - Added comprehensive debugging guide for MCP installation issues. - Implemented a session path resolver to infer content types from filenames and paths, improving usability. - Introduced tests for embeddings improvements in init and status commands to verify functionality.
373 lines
11 KiB
TypeScript
373 lines
11 KiB
TypeScript
/**
|
|
* Session Path Resolver - Smart file path to content_type resolution
|
|
* Eliminates need for --type parameter by inferring from filenames and paths
|
|
*/
|
|
|
|
import { basename, dirname, join } from 'path';
|
|
|
|
// Supported content types (from session-manager.ts)
|
|
type ContentType =
|
|
| 'session' | 'plan' | 'task' | 'summary' | 'process' | 'chat' | 'brainstorm'
|
|
| 'review-dim' | 'review-iter' | 'review-fix' | 'todo' | 'context'
|
|
| 'lite-plan' | 'lite-fix-plan' | 'exploration' | 'explorations-manifest'
|
|
| 'diagnosis' | 'diagnoses-manifest' | 'clarifications' | 'execution-context' | 'session-metadata';
|
|
|
|
export interface ResolverResult {
|
|
contentType: ContentType;
|
|
pathParams?: Record<string, string>;
|
|
resolvedPath: string; // Relative path within session directory
|
|
}
|
|
|
|
export interface ResolverContext {
|
|
sessionPath: string;
|
|
sessionLocation: 'active' | 'archived' | 'lite-plan' | 'lite-fix';
|
|
}
|
|
|
|
export class PathResolutionError extends Error {
|
|
code: 'NOT_FOUND' | 'INVALID_PATH';
|
|
suggestions: string[];
|
|
|
|
constructor(code: 'NOT_FOUND' | 'INVALID_PATH', message: string, suggestions: string[] = []) {
|
|
super(message);
|
|
this.name = 'PathResolutionError';
|
|
this.code = code;
|
|
this.suggestions = suggestions;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Task ID patterns (IMPL-*, TEST-*, DOC-*, REFACTOR-*, TASK-*)
|
|
*/
|
|
const TASK_ID_PATTERNS = [
|
|
/^(IMPL|TEST|DOC|REFACTOR|TASK)-\d+\.json$/i,
|
|
];
|
|
|
|
/**
|
|
* Summary filename pattern (*-summary.md)
|
|
*/
|
|
const SUMMARY_PATTERN = /^(.+)-summary\.md$/i;
|
|
|
|
/**
|
|
* Path prefix to content_type mapping
|
|
*/
|
|
const PATH_PREFIX_TO_CONTENT_TYPE: Record<string, ContentType> = {
|
|
'.task/': 'task',
|
|
'.summaries/': 'summary',
|
|
'.process/': 'process',
|
|
'.chat/': 'chat',
|
|
'.brainstorming/': 'brainstorm',
|
|
'.review/dimensions/': 'review-dim',
|
|
'.review/iterations/': 'review-iter',
|
|
'.review/fixes/': 'review-fix',
|
|
};
|
|
|
|
/**
|
|
* Exact filename to content_type mapping
|
|
*/
|
|
const EXACT_FILENAME_TO_CONTENT_TYPE: Record<string, ContentType> = {
|
|
'IMPL_PLAN.md': 'plan',
|
|
'TODO_LIST.md': 'todo',
|
|
'workflow-session.json': 'session',
|
|
'context-package.json': 'context',
|
|
'plan.json': 'lite-plan', // Will be overridden by session location
|
|
'fix-plan.json': 'lite-fix-plan', // Specific to lite-fix sessions
|
|
'session-metadata.json': 'session-metadata',
|
|
'clarifications.json': 'clarifications',
|
|
'execution-context.json': 'execution-context',
|
|
'explorations-manifest.json': 'explorations-manifest',
|
|
'diagnoses-manifest.json': 'diagnoses-manifest',
|
|
};
|
|
|
|
/**
|
|
* Extract task ID from filename
|
|
* Examples: IMPL-001.json → IMPL-001
|
|
*/
|
|
function extractTaskId(filename: string): string | null {
|
|
const match = filename.match(/^([A-Z]+-\d+)\.json$/i);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Extract summary task ID from filename
|
|
* Examples: IMPL-001-summary.md → IMPL-001
|
|
*/
|
|
function extractSummaryTaskId(filename: string): string | null {
|
|
const match = filename.match(SUMMARY_PATTERN);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Extract review dimension from path
|
|
* Examples: .review/dimensions/security.json → security
|
|
*/
|
|
function extractReviewDimension(filepath: string): string | null {
|
|
const match = filepath.match(/\.review\/dimensions\/(.+)\.json$/);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Extract review iteration from path
|
|
* Examples: .review/iterations/1.json → 1
|
|
*/
|
|
function extractReviewIteration(filepath: string): string | null {
|
|
const match = filepath.match(/\.review\/iterations\/(\d+)\.json$/);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Extract filename from relative path within review fixes
|
|
* Examples: .review/fixes/finding-001.json → finding-001.json
|
|
*/
|
|
function extractReviewFixFilename(filepath: string): string | null {
|
|
const match = filepath.match(/\.review\/fixes\/(.+)$/);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Extract filename from process/chat/brainstorm paths
|
|
* Examples: .process/archiving.json → archiving.json
|
|
*/
|
|
function extractProcessFilename(filepath: string, prefix: string): string | null {
|
|
const pattern = new RegExp(`^${prefix.replace(/\//g, '\\/')}(.+)$`);
|
|
const match = filepath.match(pattern);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Resolve relative path (contains '/') to content_type
|
|
*/
|
|
function resolveRelativePath(filepath: string): ResolverResult | null {
|
|
// Security: Reject path traversal attempts
|
|
if (filepath.includes('..') || filepath.startsWith('/')) {
|
|
throw new PathResolutionError(
|
|
'INVALID_PATH',
|
|
'Path traversal and absolute paths are not allowed',
|
|
['Use relative paths within the session directory']
|
|
);
|
|
}
|
|
|
|
// Check path prefix matches
|
|
for (const [prefix, contentType] of Object.entries(PATH_PREFIX_TO_CONTENT_TYPE)) {
|
|
if (filepath.startsWith(prefix)) {
|
|
const pathParams: Record<string, string> = {};
|
|
|
|
// Extract specific parameters based on content type
|
|
if (contentType === 'task') {
|
|
const taskId = extractTaskId(basename(filepath));
|
|
if (taskId) {
|
|
pathParams.task_id = taskId;
|
|
}
|
|
} else if (contentType === 'summary') {
|
|
const taskId = extractSummaryTaskId(basename(filepath));
|
|
if (taskId) {
|
|
pathParams.task_id = taskId;
|
|
}
|
|
} else if (contentType === 'review-dim') {
|
|
const dimension = extractReviewDimension(filepath);
|
|
if (dimension) {
|
|
pathParams.dimension = dimension;
|
|
}
|
|
} else if (contentType === 'review-iter') {
|
|
const iteration = extractReviewIteration(filepath);
|
|
if (iteration) {
|
|
pathParams.iteration = iteration;
|
|
}
|
|
} else if (contentType === 'review-fix') {
|
|
const filename = extractReviewFixFilename(filepath);
|
|
if (filename) {
|
|
pathParams.filename = filename;
|
|
}
|
|
} else if (contentType === 'process' || contentType === 'chat' || contentType === 'brainstorm') {
|
|
const filename = extractProcessFilename(filepath, prefix);
|
|
if (filename) {
|
|
pathParams.filename = filename;
|
|
}
|
|
}
|
|
|
|
return {
|
|
contentType,
|
|
pathParams: Object.keys(pathParams).length > 0 ? pathParams : undefined,
|
|
resolvedPath: filepath,
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Resolve simple filename (no '/') to content_type
|
|
*/
|
|
function resolveFilename(
|
|
filename: string,
|
|
context: ResolverContext
|
|
): ResolverResult | null {
|
|
// Check exact filename matches first
|
|
if (EXACT_FILENAME_TO_CONTENT_TYPE[filename]) {
|
|
let contentType = EXACT_FILENAME_TO_CONTENT_TYPE[filename];
|
|
|
|
// Override plan.json based on session location
|
|
if (filename === 'plan.json') {
|
|
if (context.sessionLocation === 'lite-plan') {
|
|
contentType = 'lite-plan';
|
|
} else if (context.sessionLocation === 'lite-fix') {
|
|
contentType = 'lite-fix-plan';
|
|
}
|
|
}
|
|
|
|
return {
|
|
contentType,
|
|
resolvedPath: filename,
|
|
};
|
|
}
|
|
|
|
// Check task ID patterns (IMPL-001.json, TEST-042.json, etc.)
|
|
for (const pattern of TASK_ID_PATTERNS) {
|
|
if (pattern.test(filename)) {
|
|
const taskId = extractTaskId(filename);
|
|
if (taskId) {
|
|
return {
|
|
contentType: 'task',
|
|
pathParams: { task_id: taskId },
|
|
resolvedPath: `.task/${filename}`,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check summary pattern (*-summary.md)
|
|
const summaryTaskId = extractSummaryTaskId(filename);
|
|
if (summaryTaskId) {
|
|
return {
|
|
contentType: 'summary',
|
|
pathParams: { task_id: summaryTaskId },
|
|
resolvedPath: `.summaries/${filename}`,
|
|
};
|
|
}
|
|
|
|
// Check exploration pattern (exploration-{angle}.json)
|
|
const explorationMatch = filename.match(/^exploration-(.+)\.json$/);
|
|
if (explorationMatch) {
|
|
return {
|
|
contentType: 'exploration',
|
|
pathParams: { angle: explorationMatch[1] },
|
|
resolvedPath: filename,
|
|
};
|
|
}
|
|
|
|
// Check diagnosis pattern (diagnosis-{angle}.json)
|
|
const diagnosisMatch = filename.match(/^diagnosis-(.+)\.json$/);
|
|
if (diagnosisMatch) {
|
|
return {
|
|
contentType: 'diagnosis',
|
|
pathParams: { angle: diagnosisMatch[1] },
|
|
resolvedPath: filename,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Main resolver function - resolves filename/path to content_type
|
|
*
|
|
* @param filename - Filename or relative path
|
|
* @param context - Session context (path and location)
|
|
* @returns Resolver result with content type, path params, and resolved path
|
|
* @throws PathResolutionError if path is invalid or cannot be resolved
|
|
*
|
|
* @example
|
|
* // Filename examples
|
|
* resolveFilePath('IMPL-001.json', context)
|
|
* // → { contentType: 'task', pathParams: {task_id: 'IMPL-001'}, resolvedPath: '.task/IMPL-001.json' }
|
|
*
|
|
* resolveFilePath('IMPL_PLAN.md', context)
|
|
* // → { contentType: 'plan', resolvedPath: 'IMPL_PLAN.md' }
|
|
*
|
|
* // Relative path examples
|
|
* resolveFilePath('.task/IMPL-001.json', context)
|
|
* // → { contentType: 'task', pathParams: {task_id: 'IMPL-001'}, resolvedPath: '.task/IMPL-001.json' }
|
|
*
|
|
* resolveFilePath('.review/dimensions/security.json', context)
|
|
* // → { contentType: 'review-dim', pathParams: {dimension: 'security'}, resolvedPath: '...' }
|
|
*/
|
|
export function resolveFilePath(
|
|
filename: string,
|
|
context: ResolverContext
|
|
): ResolverResult {
|
|
if (!filename) {
|
|
throw new PathResolutionError(
|
|
'INVALID_PATH',
|
|
'Filename is required',
|
|
['Usage: ccw session <session-id> read <filename|path>']
|
|
);
|
|
}
|
|
|
|
// Normalize path separators (handle Windows paths)
|
|
const normalizedFilename = filename.replace(/\\/g, '/');
|
|
|
|
// Security: Validate filename
|
|
if (normalizedFilename.includes('\0') || normalizedFilename.trim() === '') {
|
|
throw new PathResolutionError(
|
|
'INVALID_PATH',
|
|
'Invalid filename or path',
|
|
['Use valid filename or relative path']
|
|
);
|
|
}
|
|
|
|
// RULE 1: Relative path (contains '/')
|
|
if (normalizedFilename.includes('/')) {
|
|
const result = resolveRelativePath(normalizedFilename);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
|
|
throw new PathResolutionError(
|
|
'NOT_FOUND',
|
|
`Unknown path pattern: ${normalizedFilename}`,
|
|
[
|
|
'Supported paths:',
|
|
' .task/*.json (tasks)',
|
|
' .summaries/*.md (summaries)',
|
|
' .process/* (process files)',
|
|
' .chat/* (chat files)',
|
|
' .brainstorming/* (brainstorm files)',
|
|
' .review/dimensions/*.json (review dimensions)',
|
|
' .review/iterations/*.json (review iterations)',
|
|
' .review/fixes/* (review fixes)',
|
|
]
|
|
);
|
|
}
|
|
|
|
// RULE 2: Simple filename
|
|
const result = resolveFilename(normalizedFilename, context);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
|
|
// Resolution failed
|
|
throw new PathResolutionError(
|
|
'NOT_FOUND',
|
|
`Unknown file: ${normalizedFilename}`,
|
|
[
|
|
'Supported filenames:',
|
|
' IMPL-*.json, TEST-*.json, DOC-*.json, REFACTOR-*.json, TASK-*.json (tasks)',
|
|
' IMPL_PLAN.md (plan)',
|
|
' TODO_LIST.md (todo)',
|
|
' workflow-session.json (session metadata)',
|
|
' context-package.json (context)',
|
|
' session-metadata.json (session metadata for lite sessions)',
|
|
' plan.json (lite-plan or lite-fix-plan, depending on session location)',
|
|
' fix-plan.json (lite-fix-plan)',
|
|
' *-summary.md (summaries)',
|
|
' exploration-{angle}.json (explorations)',
|
|
' diagnosis-{angle}.json (diagnoses)',
|
|
' explorations-manifest.json (exploration manifest)',
|
|
' diagnoses-manifest.json (diagnosis manifest)',
|
|
' clarifications.json (clarifications)',
|
|
' execution-context.json (execution context)',
|
|
'Or use a relative path: .task/IMPL-001.json',
|
|
]
|
|
);
|
|
}
|