mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat(ccw-litellm): enhance status checks and add file preview functionality
This commit is contained in:
@@ -80,9 +80,89 @@ import { getContextCacheStore } from '../../tools/context-cache-store.js';
|
||||
import { getLiteLLMClient } from '../../tools/litellm-client.js';
|
||||
import { testApiKeyConnection, getDefaultApiBase } from '../services/api-key-tester.js';
|
||||
|
||||
interface CcwLitellmEnvCheck {
|
||||
python: string;
|
||||
installed: boolean;
|
||||
version?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface CcwLitellmStatusResponse {
|
||||
/**
|
||||
* Whether ccw-litellm is installed in the CodexLens venv.
|
||||
* This is the environment used for the LiteLLM embedding backend.
|
||||
*/
|
||||
installed: boolean;
|
||||
version?: string;
|
||||
error?: string;
|
||||
checks?: {
|
||||
codexLensVenv: CcwLitellmEnvCheck;
|
||||
systemPython?: CcwLitellmEnvCheck;
|
||||
};
|
||||
}
|
||||
|
||||
function checkCcwLitellmImport(
|
||||
pythonCmd: string,
|
||||
options: { timeout: number; shell?: boolean }
|
||||
): Promise<CcwLitellmEnvCheck> {
|
||||
const { timeout, shell = false } = options;
|
||||
|
||||
const sanitizePythonError = (stderrText: string): string | undefined => {
|
||||
const trimmed = stderrText.trim();
|
||||
if (!trimmed) return undefined;
|
||||
const lines = trimmed
|
||||
.split(/\r?\n/g)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
// Prefer the final exception line (avoids leaking full traceback + file paths)
|
||||
return lines[lines.length - 1] || undefined;
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(pythonCmd, ['-c', 'import ccw_litellm; print(ccw_litellm.__version__)'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout,
|
||||
windowsHide: true,
|
||||
shell,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout?.on('data', (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code: number | null) => {
|
||||
const version = stdout.trim();
|
||||
const error = sanitizePythonError(stderr);
|
||||
|
||||
if (code === 0 && version) {
|
||||
resolve({ python: pythonCmd, installed: true, version });
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === null) {
|
||||
resolve({ python: pythonCmd, installed: false, error: `Timed out after ${timeout}ms` });
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({ python: pythonCmd, installed: false, error: error || undefined });
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ python: pythonCmd, installed: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Cache for ccw-litellm status check
|
||||
let ccwLitellmStatusCache: {
|
||||
data: { installed: boolean; version?: string; error?: string } | null;
|
||||
data: CcwLitellmStatusResponse | null;
|
||||
timestamp: number;
|
||||
ttl: number;
|
||||
} = {
|
||||
@@ -849,51 +929,29 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
||||
return true;
|
||||
}
|
||||
|
||||
// Async check - use CodexLens venv Python for reliable detection
|
||||
try {
|
||||
let result: { installed: boolean; version?: string; error?: string } = { installed: false };
|
||||
|
||||
// Check ONLY in CodexLens venv (where UV installs packages)
|
||||
// Do NOT fallback to system pip - we want isolated venv dependencies
|
||||
const uv = createCodexLensUvManager();
|
||||
const venvPython = uv.getVenvPython();
|
||||
const statusTimeout = process.platform === 'win32' ? 15000 : 10000;
|
||||
const codexLensVenv = uv.isVenvValid()
|
||||
? await checkCcwLitellmImport(venvPython, { timeout: statusTimeout })
|
||||
: { python: venvPython, installed: false, error: 'CodexLens venv not valid' };
|
||||
|
||||
if (uv.isVenvValid()) {
|
||||
try {
|
||||
result = await new Promise<{ installed: boolean; version?: string }>((resolve) => {
|
||||
const child = spawn(venvPython, ['-c', 'import ccw_litellm; print(ccw_litellm.__version__)'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: statusTimeout,
|
||||
windowsHide: true,
|
||||
});
|
||||
let stdout = '';
|
||||
child.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
|
||||
child.on('close', (code: number | null) => {
|
||||
if (code === 0) {
|
||||
const version = stdout.trim();
|
||||
if (version) {
|
||||
console.log(`[ccw-litellm status] Found in CodexLens venv: ${version}`);
|
||||
resolve({ installed: true, version });
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log('[ccw-litellm status] Not found in CodexLens venv');
|
||||
resolve({ installed: false });
|
||||
});
|
||||
child.on('error', () => {
|
||||
console.log('[ccw-litellm status] Spawn error checking venv');
|
||||
resolve({ installed: false });
|
||||
});
|
||||
});
|
||||
} catch (venvErr) {
|
||||
console.log('[ccw-litellm status] Not found in CodexLens venv');
|
||||
result = { installed: false };
|
||||
}
|
||||
} else {
|
||||
console.log('[ccw-litellm status] CodexLens venv not valid');
|
||||
result = { installed: false };
|
||||
}
|
||||
// Diagnostics only: if not installed in venv, also check system python so users understand mismatches.
|
||||
// NOTE: `installed` flag remains the CodexLens venv status (we want isolated venv dependencies).
|
||||
const systemPython = !codexLensVenv.installed
|
||||
? await checkCcwLitellmImport(getSystemPython(), { timeout: statusTimeout, shell: true })
|
||||
: undefined;
|
||||
|
||||
const result: CcwLitellmStatusResponse = {
|
||||
installed: codexLensVenv.installed,
|
||||
version: codexLensVenv.version,
|
||||
error: codexLensVenv.error,
|
||||
checks: {
|
||||
codexLensVenv,
|
||||
...(systemPython ? { systemPython } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
// Update cache
|
||||
ccwLitellmStatusCache = {
|
||||
|
||||
@@ -279,27 +279,37 @@ function parseSkillFrontmatter(content: string): ParsedSkillFrontmatter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of supporting files for a skill
|
||||
* Get list of supporting files for a skill (recursive)
|
||||
* @param {string} skillDir
|
||||
* @returns {string[]}
|
||||
* @returns {string[]} Array of relative paths (directories end with '/')
|
||||
*/
|
||||
function getSupportingFiles(skillDir: string): string[] {
|
||||
const files: string[] = [];
|
||||
try {
|
||||
const entries = readdirSync(skillDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
// Exclude SKILL.md and SKILL.md.disabled from supporting files
|
||||
if (entry.name !== 'SKILL.md' && entry.name !== 'SKILL.md.disabled') {
|
||||
if (entry.isFile()) {
|
||||
files.push(entry.name);
|
||||
} else if (entry.isDirectory()) {
|
||||
files.push(entry.name + '/');
|
||||
|
||||
function readDirRecursive(dirPath: string, relativePath: string = '') {
|
||||
try {
|
||||
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
// Exclude SKILL.md and SKILL.md.disabled from supporting files
|
||||
if (entry.name !== 'SKILL.md' && entry.name !== 'SKILL.md.disabled') {
|
||||
const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
|
||||
if (entry.isFile()) {
|
||||
files.push(entryRelativePath);
|
||||
} else if (entry.isDirectory()) {
|
||||
// Add directory marker
|
||||
files.push(entryRelativePath + '/');
|
||||
// Recurse into subdirectory
|
||||
readDirRecursive(join(dirPath, entry.name), entryRelativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
readDirRecursive(skillDir);
|
||||
return files;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user