mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: add experimental support for AST parsing and static graph indexing
- Introduced CLI options for using AST grep parsers and enabling static graph relationships during indexing. - Updated configuration management to load new settings for AST parsing and static graph types. - Enhanced AST grep processor to handle imports with aliases and improve relationship tracking. - Modified TreeSitter parsers to support synthetic module scopes for better static graph persistence. - Implemented global relationship updates in the incremental indexer for static graph expansion. - Added new ArtifactTag and FloatingFileBrowser components to the frontend for improved terminal dashboard functionality. - Created utility functions for detecting CCW artifacts in terminal output with associated tests.
This commit is contained in:
@@ -2507,9 +2507,18 @@ export interface McpServer {
|
||||
scope: 'project' | 'global';
|
||||
}
|
||||
|
||||
export interface McpServerConflict {
|
||||
name: string;
|
||||
projectServer: McpServer;
|
||||
globalServer: McpServer;
|
||||
/** Runtime effective scope */
|
||||
effectiveScope: 'global' | 'project';
|
||||
}
|
||||
|
||||
export interface McpServersResponse {
|
||||
project: McpServer[];
|
||||
global: McpServer[];
|
||||
conflicts: McpServerConflict[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2618,7 +2627,6 @@ export async function fetchMcpServers(projectPath?: string): Promise<McpServersR
|
||||
const disabledSet = new Set(disabledServers);
|
||||
|
||||
const userServers = isUnknownRecord(config.userServers) ? (config.userServers as UnknownRecord) : {};
|
||||
const enterpriseServers = isUnknownRecord(config.enterpriseServers) ? (config.enterpriseServers as UnknownRecord) : {};
|
||||
|
||||
const projectServersRecord = projectConfig && isUnknownRecord(projectConfig.mcpServers)
|
||||
? (projectConfig.mcpServers as UnknownRecord)
|
||||
@@ -2635,21 +2643,34 @@ export async function fetchMcpServers(projectPath?: string): Promise<McpServersR
|
||||
});
|
||||
|
||||
const project: McpServer[] = Object.entries(projectServersRecord)
|
||||
// Avoid duplicates: if defined globally/enterprise, treat it as global
|
||||
.filter(([name]) => !(name in userServers) && !(name in enterpriseServers))
|
||||
.map(([name, raw]) => {
|
||||
const normalized = normalizeServerConfig(raw);
|
||||
return {
|
||||
name,
|
||||
...normalized,
|
||||
enabled: !disabledSet.has(name),
|
||||
scope: 'project',
|
||||
scope: 'project' as const,
|
||||
};
|
||||
});
|
||||
|
||||
// Detect conflicts: same name exists in both project and global
|
||||
const conflicts: McpServerConflict[] = [];
|
||||
for (const ps of project) {
|
||||
const gs = global.find(g => g.name === ps.name);
|
||||
if (gs) {
|
||||
conflicts.push({
|
||||
name: ps.name,
|
||||
projectServer: ps,
|
||||
globalServer: gs,
|
||||
effectiveScope: 'global',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
project,
|
||||
global,
|
||||
conflicts,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3549,6 +3570,7 @@ export interface CcwMcpConfig {
|
||||
projectRoot?: string;
|
||||
allowedDirs?: string;
|
||||
enableSandbox?: boolean;
|
||||
installedScopes: ('global' | 'project')[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3605,22 +3627,24 @@ export async function fetchCcwMcpConfig(): Promise<CcwMcpConfig> {
|
||||
try {
|
||||
const config = await fetchMcpConfig();
|
||||
|
||||
// Check if ccw-tools server exists in any config
|
||||
const installedScopes: ('global' | 'project')[] = [];
|
||||
let ccwServer: any = null;
|
||||
|
||||
// Check global servers
|
||||
// Check global/user servers
|
||||
if (config.globalServers?.['ccw-tools']) {
|
||||
installedScopes.push('global');
|
||||
ccwServer = config.globalServers['ccw-tools'];
|
||||
}
|
||||
// Check user servers
|
||||
if (!ccwServer && config.userServers?.['ccw-tools']) {
|
||||
} else if (config.userServers?.['ccw-tools']) {
|
||||
installedScopes.push('global');
|
||||
ccwServer = config.userServers['ccw-tools'];
|
||||
}
|
||||
|
||||
// Check project servers
|
||||
if (!ccwServer && config.projects) {
|
||||
if (config.projects) {
|
||||
for (const proj of Object.values(config.projects)) {
|
||||
if (proj.mcpServers?.['ccw-tools']) {
|
||||
ccwServer = proj.mcpServers['ccw-tools'];
|
||||
installedScopes.push('project');
|
||||
if (!ccwServer) ccwServer = proj.mcpServers['ccw-tools'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -3630,6 +3654,7 @@ export async function fetchCcwMcpConfig(): Promise<CcwMcpConfig> {
|
||||
return {
|
||||
isInstalled: false,
|
||||
enabledTools: [],
|
||||
installedScopes: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3646,11 +3671,13 @@ export async function fetchCcwMcpConfig(): Promise<CcwMcpConfig> {
|
||||
projectRoot: env.CCW_PROJECT_ROOT,
|
||||
allowedDirs: env.CCW_ALLOWED_DIRS,
|
||||
enableSandbox: env.CCW_ENABLE_SANDBOX === '1',
|
||||
installedScopes,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
isInstalled: false,
|
||||
enabledTools: [],
|
||||
installedScopes: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3742,6 +3769,27 @@ export async function uninstallCcwMcp(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall CCW Tools MCP server from a specific scope
|
||||
*/
|
||||
export async function uninstallCcwMcpFromScope(
|
||||
scope: 'global' | 'project',
|
||||
projectPath?: string
|
||||
): Promise<void> {
|
||||
if (scope === 'global') {
|
||||
await fetchApi('/api/mcp-remove-global-server', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ serverName: 'ccw-tools' }),
|
||||
});
|
||||
} else {
|
||||
if (!projectPath) throw new Error('projectPath required for project scope uninstall');
|
||||
await fetchApi('/api/mcp-remove-server', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ projectPath, serverName: 'ccw-tools' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CCW Tools MCP - Codex API ==========
|
||||
|
||||
/**
|
||||
@@ -3753,7 +3801,7 @@ export async function fetchCcwMcpConfigForCodex(): Promise<CcwMcpConfig> {
|
||||
const ccwServer = servers.find((s) => s.name === 'ccw-tools');
|
||||
|
||||
if (!ccwServer) {
|
||||
return { isInstalled: false, enabledTools: [] };
|
||||
return { isInstalled: false, enabledTools: [], installedScopes: [] };
|
||||
}
|
||||
|
||||
const env = ccwServer.env || {};
|
||||
@@ -3768,9 +3816,10 @@ export async function fetchCcwMcpConfigForCodex(): Promise<CcwMcpConfig> {
|
||||
projectRoot: env.CCW_PROJECT_ROOT,
|
||||
allowedDirs: env.CCW_ALLOWED_DIRS,
|
||||
enableSandbox: env.CCW_ENABLE_SANDBOX === '1',
|
||||
installedScopes: ['global'],
|
||||
};
|
||||
} catch {
|
||||
return { isInstalled: false, enabledTools: [] };
|
||||
return { isInstalled: false, enabledTools: [], installedScopes: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3856,18 +3905,39 @@ export async function updateCcwConfigForCodex(config: {
|
||||
* @param projectPath - Optional project path to filter data by workspace
|
||||
*/
|
||||
export async function fetchIndexStatus(projectPath?: string): Promise<IndexStatus> {
|
||||
const url = projectPath ? `/api/index/status?path=${encodeURIComponent(projectPath)}` : '/api/index/status';
|
||||
return fetchApi<IndexStatus>(url);
|
||||
const url = projectPath
|
||||
? `/api/codexlens/workspace-status?path=${encodeURIComponent(projectPath)}`
|
||||
: '/api/codexlens/workspace-status';
|
||||
const resp = await fetchApi<{
|
||||
success: boolean;
|
||||
hasIndex: boolean;
|
||||
fts?: { indexedFiles: number; totalFiles: number };
|
||||
}>(url);
|
||||
return {
|
||||
totalFiles: resp.fts?.totalFiles ?? 0,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
buildTime: 0,
|
||||
status: resp.hasIndex ? 'completed' : 'idle',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild index
|
||||
*/
|
||||
export async function rebuildIndex(request: IndexRebuildRequest = {}): Promise<IndexStatus> {
|
||||
return fetchApi<IndexStatus>('/api/index/rebuild', {
|
||||
await fetchApi<{ success: boolean }>('/api/codexlens/init', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
body: JSON.stringify({
|
||||
path: request.paths?.[0],
|
||||
indexType: 'vector',
|
||||
}),
|
||||
});
|
||||
return {
|
||||
totalFiles: 0,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
buildTime: 0,
|
||||
status: 'building',
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Prompt History API ==========
|
||||
|
||||
73
ccw/frontend/src/lib/ccw-artifacts.test.ts
Normal file
73
ccw/frontend/src/lib/ccw-artifacts.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { detectCcArtifacts } from './ccw-artifacts';
|
||||
|
||||
describe('ccw-artifacts', () => {
|
||||
it('returns empty array for empty input', () => {
|
||||
expect(detectCcArtifacts('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('detects workflow session artifacts', () => {
|
||||
const text = 'Created: (.workflow/active/WFS-demo/workflow-session.json)';
|
||||
expect(detectCcArtifacts(text)).toEqual([
|
||||
{ type: 'workflow-session', path: '.workflow/active/WFS-demo/workflow-session.json' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('detects lite session artifacts', () => {
|
||||
const text = 'Plan: .workflow/.lite-plan/terminal-dashboard-enhancement-2026-02-15/plan.json';
|
||||
expect(detectCcArtifacts(text)).toEqual([
|
||||
{ type: 'lite-session', path: '.workflow/.lite-plan/terminal-dashboard-enhancement-2026-02-15/plan.json' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('detects CLAUDE.md artifacts (case-insensitive)', () => {
|
||||
const text = 'Updated: /repo/docs/claude.md and also CLAUDE.md';
|
||||
const res = detectCcArtifacts(text);
|
||||
expect(res).toEqual([
|
||||
{ type: 'claude-md', path: '/repo/docs/claude.md' },
|
||||
{ type: 'claude-md', path: 'CLAUDE.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('detects CCW config artifacts', () => {
|
||||
const text = 'Config: .ccw/config.toml and ccw.config.yaml';
|
||||
expect(detectCcArtifacts(text)).toEqual([
|
||||
{ type: 'ccw-config', path: '.ccw/config.toml' },
|
||||
{ type: 'ccw-config', path: 'ccw.config.yaml' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('detects issue artifacts', () => {
|
||||
const text = 'Queue: .workflow/issues/queues/index.json';
|
||||
expect(detectCcArtifacts(text)).toEqual([
|
||||
{ type: 'issue', path: '.workflow/issues/queues/index.json' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('deduplicates repeated artifacts', () => {
|
||||
const text = '.workflow/issues/issues.jsonl ... .workflow/issues/issues.jsonl';
|
||||
expect(detectCcArtifacts(text)).toEqual([
|
||||
{ type: 'issue', path: '.workflow/issues/issues.jsonl' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves discovery order across types', () => {
|
||||
const text = [
|
||||
'Issue: .workflow/issues/issues.jsonl',
|
||||
'Then plan: .workflow/.lite-plan/abc/plan.json',
|
||||
'Then session: .workflow/active/WFS-x/workflow-session.json',
|
||||
'Then config: .ccw/config.toml',
|
||||
'Then CLAUDE: CLAUDE.md',
|
||||
].join(' | ');
|
||||
|
||||
const res = detectCcArtifacts(text);
|
||||
expect(res.map((a) => a.type)).toEqual([
|
||||
'issue',
|
||||
'lite-session',
|
||||
'workflow-session',
|
||||
'ccw-config',
|
||||
'claude-md',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
91
ccw/frontend/src/lib/ccw-artifacts.ts
Normal file
91
ccw/frontend/src/lib/ccw-artifacts.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// ========================================
|
||||
// CCW Artifacts - Types & Detection
|
||||
// ========================================
|
||||
|
||||
export type ArtifactType =
|
||||
| 'workflow-session'
|
||||
| 'lite-session'
|
||||
| 'claude-md'
|
||||
| 'ccw-config'
|
||||
| 'issue';
|
||||
|
||||
export interface CcArtifact {
|
||||
type: ArtifactType;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const TRAILING_PUNCTUATION = /[)\]}>,.;:!?]+$/g;
|
||||
const WRAP_QUOTES = /^['"`]+|['"`]+$/g;
|
||||
|
||||
function normalizePath(raw: string): string {
|
||||
return raw.trim().replace(WRAP_QUOTES, '').replace(TRAILING_PUNCTUATION, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Patterns for detecting CCW-related artifacts in terminal output.
|
||||
*
|
||||
* Notes:
|
||||
* - Prefer relative paths (e.g., `.workflow/...`) so callers can resolve against a project root.
|
||||
* - Keep patterns conservative to reduce false positives in generic logs.
|
||||
*/
|
||||
export const ARTIFACT_PATTERNS: Record<ArtifactType, RegExp[]> = {
|
||||
'workflow-session': [
|
||||
/(?:^|[^\w.])(\.workflow[\\/](?:active|archives)[\\/][^\s"'`]+[\\/]workflow-session\.json)\b/g,
|
||||
],
|
||||
'lite-session': [
|
||||
/(?:^|[^\w.])(\.workflow[\\/]\.lite-plan[\\/][^\s"'`]+)\b/g,
|
||||
],
|
||||
'claude-md': [
|
||||
/([^\s"'`]*CLAUDE\.md)\b/gi,
|
||||
],
|
||||
'ccw-config': [
|
||||
/(?:^|[^\w.])(\.ccw[\\/][^\s"'`]+)\b/g,
|
||||
/(?:^|[^\w.])(ccw\.config\.(?:json|ya?ml|toml))\b/gi,
|
||||
],
|
||||
issue: [
|
||||
/(?:^|[^\w.])(\.workflow[\\/]issues[\\/][^\s"'`]+)\b/g,
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect CCW artifacts from an arbitrary text blob.
|
||||
*
|
||||
* Returns a de-duplicated list of `{ type, path }` in discovery order.
|
||||
*/
|
||||
export function detectCcArtifacts(text: string): CcArtifact[] {
|
||||
if (!text) return [];
|
||||
|
||||
const candidates: Array<CcArtifact & { index: number }> = [];
|
||||
|
||||
for (const type of Object.keys(ARTIFACT_PATTERNS) as ArtifactType[]) {
|
||||
for (const pattern of ARTIFACT_PATTERNS[type]) {
|
||||
pattern.lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
const raw = match[1] ?? match[0];
|
||||
const path = normalizePath(raw);
|
||||
if (!path) continue;
|
||||
|
||||
const full = match[0] ?? '';
|
||||
const group = match[1] ?? raw;
|
||||
const rel = full.indexOf(group);
|
||||
const index = (match.index ?? 0) + (rel >= 0 ? rel : 0);
|
||||
|
||||
candidates.push({ type, path, index });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates.sort((a, b) => a.index - b.index);
|
||||
|
||||
const results: CcArtifact[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const c of candidates) {
|
||||
const key = `${c.type}:${c.path}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
results.push({ type: c.type, path: c.path });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user