mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-28 23:35:27 +08:00
refactor: move mcp and skills logic to dedicated services
This commit is contained in:
@@ -5,6 +5,8 @@ import { AppError } from '@/shared/utils/app-error.js';
|
|||||||
import { createApiErrorResponse, createApiSuccessResponse } from '@/shared/http/api-response.js';
|
import { createApiErrorResponse, createApiSuccessResponse } from '@/shared/http/api-response.js';
|
||||||
import { llmService } from '@/modules/ai-runtime/services/ai-runtime.service.js';
|
import { llmService } from '@/modules/ai-runtime/services/ai-runtime.service.js';
|
||||||
import { llmSessionsService } from '@/modules/ai-runtime/services/sessions.service.js';
|
import { llmSessionsService } from '@/modules/ai-runtime/services/sessions.service.js';
|
||||||
|
import { llmMcpService } from '@/modules/ai-runtime/services/mcp.service.js';
|
||||||
|
import { llmSkillsService } from '@/modules/ai-runtime/services/skills.service.js';
|
||||||
import type { McpScope, McpTransport, UpsertProviderMcpServerInput } from '@/modules/ai-runtime/types/index.js';
|
import type { McpScope, McpTransport, UpsertProviderMcpServerInput } from '@/modules/ai-runtime/types/index.js';
|
||||||
import { llmMessagesUnifier } from '@/modules/ai-runtime/services/messages-unifier.service.js';
|
import { llmMessagesUnifier } from '@/modules/ai-runtime/services/messages-unifier.service.js';
|
||||||
import type { LLMProvider } from '@/shared/types/app.js';
|
import type { LLMProvider } from '@/shared/types/app.js';
|
||||||
@@ -304,12 +306,12 @@ router.get(
|
|||||||
const scope = parseMcpScope(req.query.scope);
|
const scope = parseMcpScope(req.query.scope);
|
||||||
|
|
||||||
if (scope) {
|
if (scope) {
|
||||||
const servers = await llmService.listProviderMcpServersForScope(provider, scope, { workspacePath });
|
const servers = await llmMcpService.listProviderMcpServersForScope(provider, scope, { workspacePath });
|
||||||
res.json(createApiSuccessResponse({ provider, scope, servers }));
|
res.json(createApiSuccessResponse({ provider, scope, servers }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupedServers = await llmService.listProviderMcpServers(provider, { workspacePath });
|
const groupedServers = await llmMcpService.listProviderMcpServers(provider, { workspacePath });
|
||||||
res.json(createApiSuccessResponse({ provider, scopes: groupedServers }));
|
res.json(createApiSuccessResponse({ provider, scopes: groupedServers }));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -322,7 +324,7 @@ router.post(
|
|||||||
asyncHandler(async (req: Request, res: Response) => {
|
asyncHandler(async (req: Request, res: Response) => {
|
||||||
const provider = parseProvider(req.params.provider);
|
const provider = parseProvider(req.params.provider);
|
||||||
const payload = parseMcpUpsertPayload(req.body);
|
const payload = parseMcpUpsertPayload(req.body);
|
||||||
const server = await llmService.upsertProviderMcpServer(provider, payload);
|
const server = await llmMcpService.upsertProviderMcpServer(provider, payload);
|
||||||
res.status(201).json(createApiSuccessResponse({ server }));
|
res.status(201).json(createApiSuccessResponse({ server }));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -338,7 +340,7 @@ router.put(
|
|||||||
...((req.body && typeof req.body === 'object') ? req.body as Record<string, unknown> : {}),
|
...((req.body && typeof req.body === 'object') ? req.body as Record<string, unknown> : {}),
|
||||||
name: readPathParam(req.params.name, 'name'),
|
name: readPathParam(req.params.name, 'name'),
|
||||||
});
|
});
|
||||||
const server = await llmService.upsertProviderMcpServer(provider, payload);
|
const server = await llmMcpService.upsertProviderMcpServer(provider, payload);
|
||||||
res.json(createApiSuccessResponse({ server }));
|
res.json(createApiSuccessResponse({ server }));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -352,7 +354,7 @@ router.delete(
|
|||||||
const provider = parseProvider(req.params.provider);
|
const provider = parseProvider(req.params.provider);
|
||||||
const scope = parseMcpScope(req.query.scope);
|
const scope = parseMcpScope(req.query.scope);
|
||||||
const workspacePath = readOptionalQueryString(req.query.workspacePath);
|
const workspacePath = readOptionalQueryString(req.query.workspacePath);
|
||||||
const result = await llmService.removeProviderMcpServer(provider, {
|
const result = await llmMcpService.removeProviderMcpServer(provider, {
|
||||||
name: readPathParam(req.params.name, 'name'),
|
name: readPathParam(req.params.name, 'name'),
|
||||||
scope,
|
scope,
|
||||||
workspacePath,
|
workspacePath,
|
||||||
@@ -371,7 +373,7 @@ router.post(
|
|||||||
const body = (req.body as Record<string, unknown> | undefined) ?? {};
|
const body = (req.body as Record<string, unknown> | undefined) ?? {};
|
||||||
const scope = parseMcpScope(body.scope ?? req.query.scope);
|
const scope = parseMcpScope(body.scope ?? req.query.scope);
|
||||||
const workspacePath = readOptionalQueryString(body.workspacePath ?? req.query.workspacePath);
|
const workspacePath = readOptionalQueryString(body.workspacePath ?? req.query.workspacePath);
|
||||||
const result = await llmService.runProviderMcpServer(provider, {
|
const result = await llmMcpService.runProviderMcpServer(provider, {
|
||||||
name: readPathParam(req.params.name, 'name'),
|
name: readPathParam(req.params.name, 'name'),
|
||||||
scope,
|
scope,
|
||||||
workspacePath,
|
workspacePath,
|
||||||
@@ -393,7 +395,7 @@ router.post(
|
|||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const results = await llmService.addMcpServerToAllProviders({
|
const results = await llmMcpService.addMcpServerToAllProviders({
|
||||||
...payload,
|
...payload,
|
||||||
scope: payload.scope === 'user' ? 'user' : 'project',
|
scope: payload.scope === 'user' ? 'user' : 'project',
|
||||||
});
|
});
|
||||||
@@ -409,7 +411,7 @@ router.get(
|
|||||||
asyncHandler(async (req: Request, res: Response) => {
|
asyncHandler(async (req: Request, res: Response) => {
|
||||||
const provider = parseProvider(req.params.provider);
|
const provider = parseProvider(req.params.provider);
|
||||||
const workspacePath = readOptionalQueryString(req.query.workspacePath);
|
const workspacePath = readOptionalQueryString(req.query.workspacePath);
|
||||||
const skills = await llmService.listProviderSkills(provider, { workspacePath });
|
const skills = await llmSkillsService.listProviderSkills(provider, { workspacePath });
|
||||||
res.json(createApiSuccessResponse({ provider, skills }));
|
res.json(createApiSuccessResponse({ provider, skills }));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -424,7 +426,7 @@ router.get(
|
|||||||
const workspacePath = readOptionalQueryString(req.query.workspacePath);
|
const workspacePath = readOptionalQueryString(req.query.workspacePath);
|
||||||
if (providerQuery) {
|
if (providerQuery) {
|
||||||
const provider = parseProvider(providerQuery);
|
const provider = parseProvider(providerQuery);
|
||||||
const skills = await llmService.listProviderSkills(provider, { workspacePath });
|
const skills = await llmSkillsService.listProviderSkills(provider, { workspacePath });
|
||||||
res.json(createApiSuccessResponse({ provider, skills }));
|
res.json(createApiSuccessResponse({ provider, skills }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -434,7 +436,7 @@ router.get(
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
providers.map(async (provider) => ([
|
providers.map(async (provider) => ([
|
||||||
provider,
|
provider,
|
||||||
await llmService.listProviderSkills(provider, { workspacePath }),
|
await llmSkillsService.listProviderSkills(provider, { workspacePath }),
|
||||||
])),
|
])),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,14 +2,10 @@ import type { LLMProvider } from '@/shared/types/app.js';
|
|||||||
import { AppError } from '@/shared/utils/app-error.js';
|
import { AppError } from '@/shared/utils/app-error.js';
|
||||||
import { llmProviderRegistry } from '@/modules/ai-runtime/ai-runtime.registry.js';
|
import { llmProviderRegistry } from '@/modules/ai-runtime/ai-runtime.registry.js';
|
||||||
import type {
|
import type {
|
||||||
McpScope,
|
|
||||||
ProviderMcpServer,
|
|
||||||
ProviderModel,
|
ProviderModel,
|
||||||
ProviderSkill,
|
|
||||||
ProviderSessionSnapshot,
|
ProviderSessionSnapshot,
|
||||||
RuntimePermissionMode,
|
RuntimePermissionMode,
|
||||||
StartSessionInput,
|
StartSessionInput,
|
||||||
UpsertProviderMcpServerInput,
|
|
||||||
} from '@/modules/ai-runtime/types/index.js';
|
} from '@/modules/ai-runtime/types/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,113 +127,6 @@ export const llmService = {
|
|||||||
const provider = llmProviderRegistry.resolveProvider(providerName);
|
const provider = llmProviderRegistry.resolveProvider(providerName);
|
||||||
return provider.stopSession(sessionId);
|
return provider.stopSession(sessionId);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists MCP servers for one provider grouped by supported scopes.
|
|
||||||
*/
|
|
||||||
async listProviderMcpServers(
|
|
||||||
providerName: string,
|
|
||||||
options?: { workspacePath?: string },
|
|
||||||
): Promise<Record<McpScope, ProviderMcpServer[]>> {
|
|
||||||
const provider = llmProviderRegistry.resolveProvider(providerName);
|
|
||||||
return provider.mcp.listServers(options);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists MCP servers for one provider scope.
|
|
||||||
*/
|
|
||||||
async listProviderMcpServersForScope(
|
|
||||||
providerName: string,
|
|
||||||
scope: McpScope,
|
|
||||||
options?: { workspacePath?: string },
|
|
||||||
): Promise<ProviderMcpServer[]> {
|
|
||||||
const provider = llmProviderRegistry.resolveProvider(providerName);
|
|
||||||
return provider.mcp.listServersForScope(scope, options);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds or updates one provider MCP server.
|
|
||||||
*/
|
|
||||||
async upsertProviderMcpServer(
|
|
||||||
providerName: string,
|
|
||||||
input: UpsertProviderMcpServerInput,
|
|
||||||
): Promise<ProviderMcpServer> {
|
|
||||||
const provider = llmProviderRegistry.resolveProvider(providerName);
|
|
||||||
return provider.mcp.upsertServer(input);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes one provider MCP server.
|
|
||||||
*/
|
|
||||||
async removeProviderMcpServer(
|
|
||||||
providerName: string,
|
|
||||||
input: { name: string; scope?: McpScope; workspacePath?: string },
|
|
||||||
): Promise<{ removed: boolean; provider: LLMProvider; name: string; scope: McpScope }> {
|
|
||||||
const provider = llmProviderRegistry.resolveProvider(providerName);
|
|
||||||
return provider.mcp.removeServer(input);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs one provider MCP server probe.
|
|
||||||
*/
|
|
||||||
async runProviderMcpServer(
|
|
||||||
providerName: string,
|
|
||||||
input: { name: string; scope?: McpScope; workspacePath?: string },
|
|
||||||
): Promise<{
|
|
||||||
provider: LLMProvider;
|
|
||||||
name: string;
|
|
||||||
scope: McpScope;
|
|
||||||
transport: 'stdio' | 'http' | 'sse';
|
|
||||||
reachable: boolean;
|
|
||||||
statusCode?: number;
|
|
||||||
error?: string;
|
|
||||||
}> {
|
|
||||||
const provider = llmProviderRegistry.resolveProvider(providerName);
|
|
||||||
return provider.mcp.runServer(input);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds one HTTP/stdio MCP server to every provider.
|
|
||||||
*/
|
|
||||||
async addMcpServerToAllProviders(
|
|
||||||
input: Omit<UpsertProviderMcpServerInput, 'scope'> & { scope?: Exclude<McpScope, 'local'> },
|
|
||||||
): Promise<Array<{ provider: LLMProvider; created: boolean; error?: string }>> {
|
|
||||||
if (input.transport !== 'stdio' && input.transport !== 'http') {
|
|
||||||
throw new AppError('Global MCP add supports only "stdio" and "http".', {
|
|
||||||
code: 'INVALID_GLOBAL_MCP_TRANSPORT',
|
|
||||||
statusCode: 400,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const scope = input.scope ?? 'project';
|
|
||||||
const results: Array<{ provider: LLMProvider; created: boolean; error?: string }> = [];
|
|
||||||
const providers = llmProviderRegistry.listProviders();
|
|
||||||
for (const provider of providers) {
|
|
||||||
try {
|
|
||||||
await provider.mcp.upsertServer({ ...input, scope });
|
|
||||||
results.push({ provider: provider.id, created: true });
|
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
provider: provider.id,
|
|
||||||
created: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists skills for one provider.
|
|
||||||
*/
|
|
||||||
async listProviderSkills(
|
|
||||||
providerName: string,
|
|
||||||
options?: { workspacePath?: string },
|
|
||||||
): Promise<ProviderSkill[]> {
|
|
||||||
const provider = llmProviderRegistry.resolveProvider(providerName);
|
|
||||||
return provider.skills.listSkills(options);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
106
server/src/modules/ai-runtime/services/mcp.service.ts
Normal file
106
server/src/modules/ai-runtime/services/mcp.service.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import type { LLMProvider } from '@/shared/types/app.js';
|
||||||
|
import { AppError } from '@/shared/utils/app-error.js';
|
||||||
|
import { llmProviderRegistry } from '@/modules/ai-runtime/ai-runtime.registry.js';
|
||||||
|
import type {
|
||||||
|
McpScope,
|
||||||
|
ProviderMcpServer,
|
||||||
|
UpsertProviderMcpServerInput,
|
||||||
|
} from '@/modules/ai-runtime/types/index.js';
|
||||||
|
|
||||||
|
export const llmMcpService = {
|
||||||
|
/**
|
||||||
|
* Lists MCP servers for one provider grouped by supported scopes.
|
||||||
|
*/
|
||||||
|
async listProviderMcpServers(
|
||||||
|
providerName: string,
|
||||||
|
options?: { workspacePath?: string },
|
||||||
|
): Promise<Record<McpScope, ProviderMcpServer[]>> {
|
||||||
|
const provider = llmProviderRegistry.resolveProvider(providerName);
|
||||||
|
return provider.mcp.listServers(options);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists MCP servers for one provider scope.
|
||||||
|
*/
|
||||||
|
async listProviderMcpServersForScope(
|
||||||
|
providerName: string,
|
||||||
|
scope: McpScope,
|
||||||
|
options?: { workspacePath?: string },
|
||||||
|
): Promise<ProviderMcpServer[]> {
|
||||||
|
const provider = llmProviderRegistry.resolveProvider(providerName);
|
||||||
|
return provider.mcp.listServersForScope(scope, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or updates one provider MCP server.
|
||||||
|
*/
|
||||||
|
async upsertProviderMcpServer(
|
||||||
|
providerName: string,
|
||||||
|
input: UpsertProviderMcpServerInput,
|
||||||
|
): Promise<ProviderMcpServer> {
|
||||||
|
const provider = llmProviderRegistry.resolveProvider(providerName);
|
||||||
|
return provider.mcp.upsertServer(input);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes one provider MCP server.
|
||||||
|
*/
|
||||||
|
async removeProviderMcpServer(
|
||||||
|
providerName: string,
|
||||||
|
input: { name: string; scope?: McpScope; workspacePath?: string },
|
||||||
|
): Promise<{ removed: boolean; provider: LLMProvider; name: string; scope: McpScope }> {
|
||||||
|
const provider = llmProviderRegistry.resolveProvider(providerName);
|
||||||
|
return provider.mcp.removeServer(input);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs one provider MCP server probe.
|
||||||
|
*/
|
||||||
|
async runProviderMcpServer(
|
||||||
|
providerName: string,
|
||||||
|
input: { name: string; scope?: McpScope; workspacePath?: string },
|
||||||
|
): Promise<{
|
||||||
|
provider: LLMProvider;
|
||||||
|
name: string;
|
||||||
|
scope: McpScope;
|
||||||
|
transport: 'stdio' | 'http' | 'sse';
|
||||||
|
reachable: boolean;
|
||||||
|
statusCode?: number;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
const provider = llmProviderRegistry.resolveProvider(providerName);
|
||||||
|
return provider.mcp.runServer(input);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds one HTTP/stdio MCP server to every provider.
|
||||||
|
*/
|
||||||
|
async addMcpServerToAllProviders(
|
||||||
|
input: Omit<UpsertProviderMcpServerInput, 'scope'> & { scope?: Exclude<McpScope, 'local'> },
|
||||||
|
): Promise<Array<{ provider: LLMProvider; created: boolean; error?: string }>> {
|
||||||
|
if (input.transport !== 'stdio' && input.transport !== 'http') {
|
||||||
|
throw new AppError('Global MCP add supports only "stdio" and "http".', {
|
||||||
|
code: 'INVALID_GLOBAL_MCP_TRANSPORT',
|
||||||
|
statusCode: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = input.scope ?? 'project';
|
||||||
|
const results: Array<{ provider: LLMProvider; created: boolean; error?: string }> = [];
|
||||||
|
const providers = llmProviderRegistry.listProviders();
|
||||||
|
for (const provider of providers) {
|
||||||
|
try {
|
||||||
|
await provider.mcp.upsertServer({ ...input, scope });
|
||||||
|
results.push({ provider: provider.id, created: true });
|
||||||
|
} catch (error) {
|
||||||
|
results.push({
|
||||||
|
provider: provider.id,
|
||||||
|
created: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
};
|
||||||
15
server/src/modules/ai-runtime/services/skills.service.ts
Normal file
15
server/src/modules/ai-runtime/services/skills.service.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { llmProviderRegistry } from '@/modules/ai-runtime/ai-runtime.registry.js';
|
||||||
|
import type { ProviderSkill } from '@/modules/ai-runtime/types/index.js';
|
||||||
|
|
||||||
|
export const llmSkillsService = {
|
||||||
|
/**
|
||||||
|
* Lists skills for one provider.
|
||||||
|
*/
|
||||||
|
async listProviderSkills(
|
||||||
|
providerName: string,
|
||||||
|
options?: { workspacePath?: string },
|
||||||
|
): Promise<ProviderSkill[]> {
|
||||||
|
const provider = llmProviderRegistry.resolveProvider(providerName);
|
||||||
|
return provider.skills.listSkills(options);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ import test from 'node:test';
|
|||||||
import TOML from '@iarna/toml';
|
import TOML from '@iarna/toml';
|
||||||
|
|
||||||
import { AppError } from '@/shared/utils/app-error.js';
|
import { AppError } from '@/shared/utils/app-error.js';
|
||||||
import { llmService } from '@/modules/ai-runtime/services/ai-runtime.service.js';
|
import { llmMcpService } from '@/modules/ai-runtime/services/mcp.service.js';
|
||||||
|
|
||||||
const patchHomeDir = (nextHomeDir: string) => {
|
const patchHomeDir = (nextHomeDir: string) => {
|
||||||
const original = os.homedir;
|
const original = os.homedir;
|
||||||
@@ -27,14 +27,14 @@ const readJson = async (filePath: string): Promise<Record<string, unknown>> => {
|
|||||||
* This test covers Claude MCP support for all scopes (user/local/project) and all transports (stdio/http/sse),
|
* This test covers Claude MCP support for all scopes (user/local/project) and all transports (stdio/http/sse),
|
||||||
* including add, update/list, and remove operations.
|
* including add, update/list, and remove operations.
|
||||||
*/
|
*/
|
||||||
test('llmService handles claude MCP scopes/transports with file-backed persistence', { concurrency: false }, async () => {
|
test('llmMcpService handles claude MCP scopes/transports with file-backed persistence', { concurrency: false }, async () => {
|
||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-claude-'));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-claude-'));
|
||||||
const workspacePath = path.join(tempRoot, 'workspace');
|
const workspacePath = path.join(tempRoot, 'workspace');
|
||||||
await fs.mkdir(workspacePath, { recursive: true });
|
await fs.mkdir(workspacePath, { recursive: true });
|
||||||
|
|
||||||
const restoreHomeDir = patchHomeDir(tempRoot);
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
||||||
try {
|
try {
|
||||||
await llmService.upsertProviderMcpServer('claude', {
|
await llmMcpService.upsertProviderMcpServer('claude', {
|
||||||
name: 'claude-user-stdio',
|
name: 'claude-user-stdio',
|
||||||
scope: 'user',
|
scope: 'user',
|
||||||
transport: 'stdio',
|
transport: 'stdio',
|
||||||
@@ -43,7 +43,7 @@ test('llmService handles claude MCP scopes/transports with file-backed persisten
|
|||||||
env: { API_KEY: 'secret' },
|
env: { API_KEY: 'secret' },
|
||||||
});
|
});
|
||||||
|
|
||||||
await llmService.upsertProviderMcpServer('claude', {
|
await llmMcpService.upsertProviderMcpServer('claude', {
|
||||||
name: 'claude-local-http',
|
name: 'claude-local-http',
|
||||||
scope: 'local',
|
scope: 'local',
|
||||||
transport: 'http',
|
transport: 'http',
|
||||||
@@ -52,7 +52,7 @@ test('llmService handles claude MCP scopes/transports with file-backed persisten
|
|||||||
workspacePath,
|
workspacePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await llmService.upsertProviderMcpServer('claude', {
|
await llmMcpService.upsertProviderMcpServer('claude', {
|
||||||
name: 'claude-project-sse',
|
name: 'claude-project-sse',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
transport: 'sse',
|
transport: 'sse',
|
||||||
@@ -61,13 +61,13 @@ test('llmService handles claude MCP scopes/transports with file-backed persisten
|
|||||||
workspacePath,
|
workspacePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const grouped = await llmService.listProviderMcpServers('claude', { workspacePath });
|
const grouped = await llmMcpService.listProviderMcpServers('claude', { workspacePath });
|
||||||
assert.ok(grouped.user.some((server) => server.name === 'claude-user-stdio' && server.transport === 'stdio'));
|
assert.ok(grouped.user.some((server) => server.name === 'claude-user-stdio' && server.transport === 'stdio'));
|
||||||
assert.ok(grouped.local.some((server) => server.name === 'claude-local-http' && server.transport === 'http'));
|
assert.ok(grouped.local.some((server) => server.name === 'claude-local-http' && server.transport === 'http'));
|
||||||
assert.ok(grouped.project.some((server) => server.name === 'claude-project-sse' && server.transport === 'sse'));
|
assert.ok(grouped.project.some((server) => server.name === 'claude-project-sse' && server.transport === 'sse'));
|
||||||
|
|
||||||
// update behavior is the same upsert route with same name
|
// update behavior is the same upsert route with same name
|
||||||
await llmService.upsertProviderMcpServer('claude', {
|
await llmMcpService.upsertProviderMcpServer('claude', {
|
||||||
name: 'claude-project-sse',
|
name: 'claude-project-sse',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
transport: 'sse',
|
transport: 'sse',
|
||||||
@@ -81,7 +81,7 @@ test('llmService handles claude MCP scopes/transports with file-backed persisten
|
|||||||
const projectServer = projectServers['claude-project-sse'] as Record<string, unknown>;
|
const projectServer = projectServers['claude-project-sse'] as Record<string, unknown>;
|
||||||
assert.equal(projectServer.url, 'https://example.com/sse-updated');
|
assert.equal(projectServer.url, 'https://example.com/sse-updated');
|
||||||
|
|
||||||
const removeResult = await llmService.removeProviderMcpServer('claude', {
|
const removeResult = await llmMcpService.removeProviderMcpServer('claude', {
|
||||||
name: 'claude-local-http',
|
name: 'claude-local-http',
|
||||||
scope: 'local',
|
scope: 'local',
|
||||||
workspacePath,
|
workspacePath,
|
||||||
@@ -97,14 +97,14 @@ test('llmService handles claude MCP scopes/transports with file-backed persisten
|
|||||||
* This test covers Codex MCP support for user/project scopes, stdio/http formats,
|
* This test covers Codex MCP support for user/project scopes, stdio/http formats,
|
||||||
* and validation for unsupported scope/transport combinations.
|
* and validation for unsupported scope/transport combinations.
|
||||||
*/
|
*/
|
||||||
test('llmService handles codex MCP TOML config and capability validation', { concurrency: false }, async () => {
|
test('llmMcpService handles codex MCP TOML config and capability validation', { concurrency: false }, async () => {
|
||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-codex-'));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-codex-'));
|
||||||
const workspacePath = path.join(tempRoot, 'workspace');
|
const workspacePath = path.join(tempRoot, 'workspace');
|
||||||
await fs.mkdir(workspacePath, { recursive: true });
|
await fs.mkdir(workspacePath, { recursive: true });
|
||||||
|
|
||||||
const restoreHomeDir = patchHomeDir(tempRoot);
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
||||||
try {
|
try {
|
||||||
await llmService.upsertProviderMcpServer('codex', {
|
await llmMcpService.upsertProviderMcpServer('codex', {
|
||||||
name: 'codex-user-stdio',
|
name: 'codex-user-stdio',
|
||||||
scope: 'user',
|
scope: 'user',
|
||||||
transport: 'stdio',
|
transport: 'stdio',
|
||||||
@@ -115,7 +115,7 @@ test('llmService handles codex MCP TOML config and capability validation', { con
|
|||||||
cwd: '/tmp',
|
cwd: '/tmp',
|
||||||
});
|
});
|
||||||
|
|
||||||
await llmService.upsertProviderMcpServer('codex', {
|
await llmMcpService.upsertProviderMcpServer('codex', {
|
||||||
name: 'codex-project-http',
|
name: 'codex-project-http',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
transport: 'http',
|
transport: 'http',
|
||||||
@@ -139,7 +139,7 @@ test('llmService handles codex MCP TOML config and capability validation', { con
|
|||||||
assert.equal(projectHttp.url, 'https://codex.example.com/mcp');
|
assert.equal(projectHttp.url, 'https://codex.example.com/mcp');
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
llmService.upsertProviderMcpServer('codex', {
|
llmMcpService.upsertProviderMcpServer('codex', {
|
||||||
name: 'codex-local',
|
name: 'codex-local',
|
||||||
scope: 'local',
|
scope: 'local',
|
||||||
transport: 'stdio',
|
transport: 'stdio',
|
||||||
@@ -152,7 +152,7 @@ test('llmService handles codex MCP TOML config and capability validation', { con
|
|||||||
);
|
);
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
llmService.upsertProviderMcpServer('codex', {
|
llmMcpService.upsertProviderMcpServer('codex', {
|
||||||
name: 'codex-sse',
|
name: 'codex-sse',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
transport: 'sse',
|
transport: 'sse',
|
||||||
@@ -173,14 +173,14 @@ test('llmService handles codex MCP TOML config and capability validation', { con
|
|||||||
/**
|
/**
|
||||||
* This test covers Gemini/Cursor MCP JSON formats and user/project scope persistence.
|
* This test covers Gemini/Cursor MCP JSON formats and user/project scope persistence.
|
||||||
*/
|
*/
|
||||||
test('llmService handles gemini and cursor MCP JSON config formats', { concurrency: false }, async () => {
|
test('llmMcpService handles gemini and cursor MCP JSON config formats', { concurrency: false }, async () => {
|
||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-gc-'));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-gc-'));
|
||||||
const workspacePath = path.join(tempRoot, 'workspace');
|
const workspacePath = path.join(tempRoot, 'workspace');
|
||||||
await fs.mkdir(workspacePath, { recursive: true });
|
await fs.mkdir(workspacePath, { recursive: true });
|
||||||
|
|
||||||
const restoreHomeDir = patchHomeDir(tempRoot);
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
||||||
try {
|
try {
|
||||||
await llmService.upsertProviderMcpServer('gemini', {
|
await llmMcpService.upsertProviderMcpServer('gemini', {
|
||||||
name: 'gemini-stdio',
|
name: 'gemini-stdio',
|
||||||
scope: 'user',
|
scope: 'user',
|
||||||
transport: 'stdio',
|
transport: 'stdio',
|
||||||
@@ -190,7 +190,7 @@ test('llmService handles gemini and cursor MCP JSON config formats', { concurren
|
|||||||
cwd: './server',
|
cwd: './server',
|
||||||
});
|
});
|
||||||
|
|
||||||
await llmService.upsertProviderMcpServer('gemini', {
|
await llmMcpService.upsertProviderMcpServer('gemini', {
|
||||||
name: 'gemini-http',
|
name: 'gemini-http',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
transport: 'http',
|
transport: 'http',
|
||||||
@@ -199,7 +199,7 @@ test('llmService handles gemini and cursor MCP JSON config formats', { concurren
|
|||||||
workspacePath,
|
workspacePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await llmService.upsertProviderMcpServer('cursor', {
|
await llmMcpService.upsertProviderMcpServer('cursor', {
|
||||||
name: 'cursor-stdio',
|
name: 'cursor-stdio',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
transport: 'stdio',
|
transport: 'stdio',
|
||||||
@@ -209,7 +209,7 @@ test('llmService handles gemini and cursor MCP JSON config formats', { concurren
|
|||||||
workspacePath,
|
workspacePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await llmService.upsertProviderMcpServer('cursor', {
|
await llmMcpService.upsertProviderMcpServer('cursor', {
|
||||||
name: 'cursor-http',
|
name: 'cursor-http',
|
||||||
scope: 'user',
|
scope: 'user',
|
||||||
transport: 'http',
|
transport: 'http',
|
||||||
@@ -240,14 +240,14 @@ test('llmService handles gemini and cursor MCP JSON config formats', { concurren
|
|||||||
* This test covers the global MCP adder requirement: only http/stdio are allowed and
|
* This test covers the global MCP adder requirement: only http/stdio are allowed and
|
||||||
* one payload is written to all providers.
|
* one payload is written to all providers.
|
||||||
*/
|
*/
|
||||||
test('llmService global adder writes to all providers and rejects unsupported transports', { concurrency: false }, async () => {
|
test('llmMcpService global adder writes to all providers and rejects unsupported transports', { concurrency: false }, async () => {
|
||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-global-'));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-global-'));
|
||||||
const workspacePath = path.join(tempRoot, 'workspace');
|
const workspacePath = path.join(tempRoot, 'workspace');
|
||||||
await fs.mkdir(workspacePath, { recursive: true });
|
await fs.mkdir(workspacePath, { recursive: true });
|
||||||
|
|
||||||
const restoreHomeDir = patchHomeDir(tempRoot);
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
||||||
try {
|
try {
|
||||||
const globalResult = await llmService.addMcpServerToAllProviders({
|
const globalResult = await llmMcpService.addMcpServerToAllProviders({
|
||||||
name: 'global-http',
|
name: 'global-http',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
transport: 'http',
|
transport: 'http',
|
||||||
@@ -271,7 +271,7 @@ test('llmService global adder writes to all providers and rejects unsupported tr
|
|||||||
assert.ok((cursorProject.mcpServers as Record<string, unknown>)['global-http']);
|
assert.ok((cursorProject.mcpServers as Record<string, unknown>)['global-http']);
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
llmService.addMcpServerToAllProviders({
|
llmMcpService.addMcpServerToAllProviders({
|
||||||
name: 'global-sse',
|
name: 'global-sse',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
transport: 'sse',
|
transport: 'sse',
|
||||||
@@ -292,7 +292,7 @@ test('llmService global adder writes to all providers and rejects unsupported tr
|
|||||||
/**
|
/**
|
||||||
* This test covers "run" behavior for both stdio and http MCP servers.
|
* This test covers "run" behavior for both stdio and http MCP servers.
|
||||||
*/
|
*/
|
||||||
test('llmService runProviderServer probes stdio and http MCP servers', { concurrency: false }, async () => {
|
test('llmMcpService runProviderServer probes stdio and http MCP servers', { concurrency: false }, async () => {
|
||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-run-'));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-run-'));
|
||||||
const workspacePath = path.join(tempRoot, 'workspace');
|
const workspacePath = path.join(tempRoot, 'workspace');
|
||||||
await fs.mkdir(workspacePath, { recursive: true });
|
await fs.mkdir(workspacePath, { recursive: true });
|
||||||
@@ -309,7 +309,7 @@ test('llmService runProviderServer probes stdio and http MCP servers', { concurr
|
|||||||
assert.ok(address && typeof address === 'object');
|
assert.ok(address && typeof address === 'object');
|
||||||
const url = `http://127.0.0.1:${address.port}/mcp`;
|
const url = `http://127.0.0.1:${address.port}/mcp`;
|
||||||
|
|
||||||
await llmService.upsertProviderMcpServer('gemini', {
|
await llmMcpService.upsertProviderMcpServer('gemini', {
|
||||||
name: 'probe-http',
|
name: 'probe-http',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
transport: 'http',
|
transport: 'http',
|
||||||
@@ -317,7 +317,7 @@ test('llmService runProviderServer probes stdio and http MCP servers', { concurr
|
|||||||
workspacePath,
|
workspacePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await llmService.upsertProviderMcpServer('cursor', {
|
await llmMcpService.upsertProviderMcpServer('cursor', {
|
||||||
name: 'probe-stdio',
|
name: 'probe-stdio',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
transport: 'stdio',
|
transport: 'stdio',
|
||||||
@@ -326,7 +326,7 @@ test('llmService runProviderServer probes stdio and http MCP servers', { concurr
|
|||||||
workspacePath,
|
workspacePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const httpProbe = await llmService.runProviderMcpServer('gemini', {
|
const httpProbe = await llmMcpService.runProviderMcpServer('gemini', {
|
||||||
name: 'probe-http',
|
name: 'probe-http',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
workspacePath,
|
workspacePath,
|
||||||
@@ -334,7 +334,7 @@ test('llmService runProviderServer probes stdio and http MCP servers', { concurr
|
|||||||
assert.equal(httpProbe.reachable, true);
|
assert.equal(httpProbe.reachable, true);
|
||||||
assert.equal(httpProbe.transport, 'http');
|
assert.equal(httpProbe.transport, 'http');
|
||||||
|
|
||||||
const stdioProbe = await llmService.runProviderMcpServer('cursor', {
|
const stdioProbe = await llmMcpService.runProviderMcpServer('cursor', {
|
||||||
name: 'probe-stdio',
|
name: 'probe-stdio',
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
workspacePath,
|
workspacePath,
|
||||||
@@ -347,3 +347,4 @@ test('llmService runProviderServer probes stdio and http MCP servers', { concurr
|
|||||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
import { llmService } from '@/modules/ai-runtime/services/ai-runtime.service.js';
|
import { llmSkillsService } from '@/modules/ai-runtime/services/skills.service.js';
|
||||||
|
|
||||||
const patchHomeDir = (nextHomeDir: string) => {
|
const patchHomeDir = (nextHomeDir: string) => {
|
||||||
const original = os.homedir;
|
const original = os.homedir;
|
||||||
@@ -34,7 +34,7 @@ const createSkill = async (
|
|||||||
/**
|
/**
|
||||||
* This test covers Claude skills fetching from user/project/plugin locations and plugin namespace invocation.
|
* This test covers Claude skills fetching from user/project/plugin locations and plugin namespace invocation.
|
||||||
*/
|
*/
|
||||||
test('llmService lists claude user/project/plugin skills with proper invocation names', { concurrency: false }, async () => {
|
test('llmSkillsService lists claude user/project/plugin skills with proper invocation names', { concurrency: false }, async () => {
|
||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-claude-'));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-claude-'));
|
||||||
const workspacePath = path.join(tempRoot, 'workspace');
|
const workspacePath = path.join(tempRoot, 'workspace');
|
||||||
const pluginInstallPath = path.join(tempRoot, 'plugin-install');
|
const pluginInstallPath = path.join(tempRoot, 'plugin-install');
|
||||||
@@ -80,7 +80,7 @@ test('llmService lists claude user/project/plugin skills with proper invocation
|
|||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
|
|
||||||
const skills = await llmService.listProviderSkills('claude', { workspacePath });
|
const skills = await llmSkillsService.listProviderSkills('claude', { workspacePath });
|
||||||
assert.ok(skills.some((skill) => skill.scope === 'user' && skill.invocation === '/user-helper'));
|
assert.ok(skills.some((skill) => skill.scope === 'user' && skill.invocation === '/user-helper'));
|
||||||
assert.ok(skills.some((skill) => skill.scope === 'project' && skill.invocation === '/project-helper'));
|
assert.ok(skills.some((skill) => skill.scope === 'project' && skill.invocation === '/project-helper'));
|
||||||
assert.ok(skills.some((skill) => skill.scope === 'plugin' && skill.invocation === '/example-skills:plugin-helper'));
|
assert.ok(skills.some((skill) => skill.scope === 'plugin' && skill.invocation === '/example-skills:plugin-helper'));
|
||||||
@@ -93,7 +93,7 @@ test('llmService lists claude user/project/plugin skills with proper invocation
|
|||||||
/**
|
/**
|
||||||
* This test covers Codex skills discovery across repo/user/system locations and `$` invocation prefix.
|
* This test covers Codex skills discovery across repo/user/system locations and `$` invocation prefix.
|
||||||
*/
|
*/
|
||||||
test('llmService lists codex skills from repo/user/system locations with dollar invocation', { concurrency: false }, async () => {
|
test('llmSkillsService lists codex skills from repo/user/system locations with dollar invocation', { concurrency: false }, async () => {
|
||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-codex-'));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-codex-'));
|
||||||
const repoRoot = path.join(tempRoot, 'repo');
|
const repoRoot = path.join(tempRoot, 'repo');
|
||||||
const workspacePath = path.join(repoRoot, 'packages', 'app');
|
const workspacePath = path.join(repoRoot, 'packages', 'app');
|
||||||
@@ -123,7 +123,7 @@ test('llmService lists codex skills from repo/user/system locations with dollar
|
|||||||
description: 'system skill',
|
description: 'system skill',
|
||||||
});
|
});
|
||||||
|
|
||||||
const skills = await llmService.listProviderSkills('codex', { workspacePath });
|
const skills = await llmSkillsService.listProviderSkills('codex', { workspacePath });
|
||||||
assert.ok(skills.some((skill) => skill.name === 'cwd-skill' && skill.invocation === '$cwd-skill'));
|
assert.ok(skills.some((skill) => skill.name === 'cwd-skill' && skill.invocation === '$cwd-skill'));
|
||||||
assert.ok(skills.some((skill) => skill.name === 'parent-skill' && skill.invocation === '$parent-skill'));
|
assert.ok(skills.some((skill) => skill.name === 'parent-skill' && skill.invocation === '$parent-skill'));
|
||||||
assert.ok(skills.some((skill) => skill.name === 'repo-root-skill' && skill.invocation === '$repo-root-skill'));
|
assert.ok(skills.some((skill) => skill.name === 'repo-root-skill' && skill.invocation === '$repo-root-skill'));
|
||||||
@@ -138,7 +138,7 @@ test('llmService lists codex skills from repo/user/system locations with dollar
|
|||||||
/**
|
/**
|
||||||
* This test covers Gemini skill fetch locations and slash-based invocation format.
|
* This test covers Gemini skill fetch locations and slash-based invocation format.
|
||||||
*/
|
*/
|
||||||
test('llmService lists gemini skills from documented directories', { concurrency: false }, async () => {
|
test('llmSkillsService lists gemini skills from documented directories', { concurrency: false }, async () => {
|
||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-gemini-'));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-gemini-'));
|
||||||
const workspacePath = path.join(tempRoot, 'workspace');
|
const workspacePath = path.join(tempRoot, 'workspace');
|
||||||
await fs.mkdir(workspacePath, { recursive: true });
|
await fs.mkdir(workspacePath, { recursive: true });
|
||||||
@@ -162,7 +162,7 @@ test('llmService lists gemini skills from documented directories', { concurrency
|
|||||||
description: 'project agents skill',
|
description: 'project agents skill',
|
||||||
});
|
});
|
||||||
|
|
||||||
const skills = await llmService.listProviderSkills('gemini', { workspacePath });
|
const skills = await llmSkillsService.listProviderSkills('gemini', { workspacePath });
|
||||||
assert.ok(skills.some((skill) => skill.invocation === '/home-gemini'));
|
assert.ok(skills.some((skill) => skill.invocation === '/home-gemini'));
|
||||||
assert.ok(skills.some((skill) => skill.invocation === '/home-agents'));
|
assert.ok(skills.some((skill) => skill.invocation === '/home-agents'));
|
||||||
assert.ok(skills.some((skill) => skill.invocation === '/project-gemini'));
|
assert.ok(skills.some((skill) => skill.invocation === '/project-gemini'));
|
||||||
@@ -176,7 +176,7 @@ test('llmService lists gemini skills from documented directories', { concurrency
|
|||||||
/**
|
/**
|
||||||
* This test covers Cursor skill fetch locations and slash-based invocation format.
|
* This test covers Cursor skill fetch locations and slash-based invocation format.
|
||||||
*/
|
*/
|
||||||
test('llmService lists cursor skills from documented directories', { concurrency: false }, async () => {
|
test('llmSkillsService lists cursor skills from documented directories', { concurrency: false }, async () => {
|
||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-cursor-'));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-cursor-'));
|
||||||
const workspacePath = path.join(tempRoot, 'workspace');
|
const workspacePath = path.join(tempRoot, 'workspace');
|
||||||
await fs.mkdir(workspacePath, { recursive: true });
|
await fs.mkdir(workspacePath, { recursive: true });
|
||||||
@@ -196,7 +196,7 @@ test('llmService lists cursor skills from documented directories', { concurrency
|
|||||||
description: 'user cursor skill',
|
description: 'user cursor skill',
|
||||||
});
|
});
|
||||||
|
|
||||||
const skills = await llmService.listProviderSkills('cursor', { workspacePath });
|
const skills = await llmSkillsService.listProviderSkills('cursor', { workspacePath });
|
||||||
assert.ok(skills.some((skill) => skill.invocation === '/project-agents'));
|
assert.ok(skills.some((skill) => skill.invocation === '/project-agents'));
|
||||||
assert.ok(skills.some((skill) => skill.invocation === '/project-cursor'));
|
assert.ok(skills.some((skill) => skill.invocation === '/project-cursor'));
|
||||||
assert.ok(skills.some((skill) => skill.invocation === '/user-cursor'));
|
assert.ok(skills.some((skill) => skill.invocation === '/user-cursor'));
|
||||||
@@ -205,3 +205,4 @@ test('llmService lists cursor skills from documented directories', { concurrency
|
|||||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user