diff --git a/server/src/modules/ai-runtime/ai-runtime.routes.ts b/server/src/modules/ai-runtime/ai-runtime.routes.ts index b2a93c92..e65567a0 100644 --- a/server/src/modules/ai-runtime/ai-runtime.routes.ts +++ b/server/src/modules/ai-runtime/ai-runtime.routes.ts @@ -5,6 +5,8 @@ import { AppError } from '@/shared/utils/app-error.js'; import { createApiErrorResponse, createApiSuccessResponse } from '@/shared/http/api-response.js'; import { llmService } from '@/modules/ai-runtime/services/ai-runtime.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 { llmMessagesUnifier } from '@/modules/ai-runtime/services/messages-unifier.service.js'; import type { LLMProvider } from '@/shared/types/app.js'; @@ -304,12 +306,12 @@ router.get( const scope = parseMcpScope(req.query.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 })); return; } - const groupedServers = await llmService.listProviderMcpServers(provider, { workspacePath }); + const groupedServers = await llmMcpService.listProviderMcpServers(provider, { workspacePath }); res.json(createApiSuccessResponse({ provider, scopes: groupedServers })); }), ); @@ -322,7 +324,7 @@ router.post( asyncHandler(async (req: Request, res: Response) => { const provider = parseProvider(req.params.provider); 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 })); }), ); @@ -338,7 +340,7 @@ router.put( ...((req.body && typeof req.body === 'object') ? req.body as Record : {}), name: readPathParam(req.params.name, 'name'), }); - const server = await llmService.upsertProviderMcpServer(provider, payload); + const server = await llmMcpService.upsertProviderMcpServer(provider, payload); res.json(createApiSuccessResponse({ server })); }), ); @@ -352,7 +354,7 @@ router.delete( const provider = parseProvider(req.params.provider); const scope = parseMcpScope(req.query.scope); const workspacePath = readOptionalQueryString(req.query.workspacePath); - const result = await llmService.removeProviderMcpServer(provider, { + const result = await llmMcpService.removeProviderMcpServer(provider, { name: readPathParam(req.params.name, 'name'), scope, workspacePath, @@ -371,7 +373,7 @@ router.post( const body = (req.body as Record | undefined) ?? {}; const scope = parseMcpScope(body.scope ?? req.query.scope); 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'), scope, workspacePath, @@ -393,7 +395,7 @@ router.post( statusCode: 400, }); } - const results = await llmService.addMcpServerToAllProviders({ + const results = await llmMcpService.addMcpServerToAllProviders({ ...payload, scope: payload.scope === 'user' ? 'user' : 'project', }); @@ -409,7 +411,7 @@ router.get( asyncHandler(async (req: Request, res: Response) => { const provider = parseProvider(req.params.provider); 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 })); }), ); @@ -424,7 +426,7 @@ router.get( const workspacePath = readOptionalQueryString(req.query.workspacePath); if (providerQuery) { const provider = parseProvider(providerQuery); - const skills = await llmService.listProviderSkills(provider, { workspacePath }); + const skills = await llmSkillsService.listProviderSkills(provider, { workspacePath }); res.json(createApiSuccessResponse({ provider, skills })); return; } @@ -434,7 +436,7 @@ router.get( await Promise.all( providers.map(async (provider) => ([ provider, - await llmService.listProviderSkills(provider, { workspacePath }), + await llmSkillsService.listProviderSkills(provider, { workspacePath }), ])), ), ); diff --git a/server/src/modules/ai-runtime/services/ai-runtime.service.ts b/server/src/modules/ai-runtime/services/ai-runtime.service.ts index 2466dbc6..fa2ea340 100644 --- a/server/src/modules/ai-runtime/services/ai-runtime.service.ts +++ b/server/src/modules/ai-runtime/services/ai-runtime.service.ts @@ -2,14 +2,10 @@ 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, ProviderModel, - ProviderSkill, ProviderSessionSnapshot, RuntimePermissionMode, StartSessionInput, - UpsertProviderMcpServerInput, } from '@/modules/ai-runtime/types/index.js'; /** @@ -131,113 +127,6 @@ export const llmService = { const provider = llmProviderRegistry.resolveProvider(providerName); return provider.stopSession(sessionId); }, - - /** - * Lists MCP servers for one provider grouped by supported scopes. - */ - async listProviderMcpServers( - providerName: string, - options?: { workspacePath?: string }, - ): Promise> { - 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 { - 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 { - 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 & { scope?: Exclude }, - ): Promise> { - 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 { - const provider = llmProviderRegistry.resolveProvider(providerName); - return provider.skills.listSkills(options); - }, }; /** diff --git a/server/src/modules/ai-runtime/services/mcp.service.ts b/server/src/modules/ai-runtime/services/mcp.service.ts new file mode 100644 index 00000000..ee459ca2 --- /dev/null +++ b/server/src/modules/ai-runtime/services/mcp.service.ts @@ -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> { + 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 { + 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 { + 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 & { scope?: Exclude }, + ): Promise> { + 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; + }, +}; diff --git a/server/src/modules/ai-runtime/services/skills.service.ts b/server/src/modules/ai-runtime/services/skills.service.ts new file mode 100644 index 00000000..895ddce9 --- /dev/null +++ b/server/src/modules/ai-runtime/services/skills.service.ts @@ -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 { + const provider = llmProviderRegistry.resolveProvider(providerName); + return provider.skills.listSkills(options); + }, +}; diff --git a/server/src/modules/ai-runtime/tests/mcp.test.ts b/server/src/modules/ai-runtime/tests/mcp.test.ts index e1b5cc72..e9a159b3 100644 --- a/server/src/modules/ai-runtime/tests/mcp.test.ts +++ b/server/src/modules/ai-runtime/tests/mcp.test.ts @@ -8,7 +8,7 @@ import test from 'node:test'; import TOML from '@iarna/toml'; 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 original = os.homedir; @@ -27,14 +27,14 @@ const readJson = async (filePath: string): Promise> => { * 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. */ -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 workspacePath = path.join(tempRoot, 'workspace'); await fs.mkdir(workspacePath, { recursive: true }); const restoreHomeDir = patchHomeDir(tempRoot); try { - await llmService.upsertProviderMcpServer('claude', { + await llmMcpService.upsertProviderMcpServer('claude', { name: 'claude-user-stdio', scope: 'user', transport: 'stdio', @@ -43,7 +43,7 @@ test('llmService handles claude MCP scopes/transports with file-backed persisten env: { API_KEY: 'secret' }, }); - await llmService.upsertProviderMcpServer('claude', { + await llmMcpService.upsertProviderMcpServer('claude', { name: 'claude-local-http', scope: 'local', transport: 'http', @@ -52,7 +52,7 @@ test('llmService handles claude MCP scopes/transports with file-backed persisten workspacePath, }); - await llmService.upsertProviderMcpServer('claude', { + await llmMcpService.upsertProviderMcpServer('claude', { name: 'claude-project-sse', scope: 'project', transport: 'sse', @@ -61,13 +61,13 @@ test('llmService handles claude MCP scopes/transports with file-backed persisten 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.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')); // update behavior is the same upsert route with same name - await llmService.upsertProviderMcpServer('claude', { + await llmMcpService.upsertProviderMcpServer('claude', { name: 'claude-project-sse', scope: 'project', 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; 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', scope: 'local', 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, * 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 workspacePath = path.join(tempRoot, 'workspace'); await fs.mkdir(workspacePath, { recursive: true }); const restoreHomeDir = patchHomeDir(tempRoot); try { - await llmService.upsertProviderMcpServer('codex', { + await llmMcpService.upsertProviderMcpServer('codex', { name: 'codex-user-stdio', scope: 'user', transport: 'stdio', @@ -115,7 +115,7 @@ test('llmService handles codex MCP TOML config and capability validation', { con cwd: '/tmp', }); - await llmService.upsertProviderMcpServer('codex', { + await llmMcpService.upsertProviderMcpServer('codex', { name: 'codex-project-http', scope: 'project', 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'); await assert.rejects( - llmService.upsertProviderMcpServer('codex', { + llmMcpService.upsertProviderMcpServer('codex', { name: 'codex-local', scope: 'local', transport: 'stdio', @@ -152,7 +152,7 @@ test('llmService handles codex MCP TOML config and capability validation', { con ); await assert.rejects( - llmService.upsertProviderMcpServer('codex', { + llmMcpService.upsertProviderMcpServer('codex', { name: 'codex-sse', scope: 'project', 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. */ -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 workspacePath = path.join(tempRoot, 'workspace'); await fs.mkdir(workspacePath, { recursive: true }); const restoreHomeDir = patchHomeDir(tempRoot); try { - await llmService.upsertProviderMcpServer('gemini', { + await llmMcpService.upsertProviderMcpServer('gemini', { name: 'gemini-stdio', scope: 'user', transport: 'stdio', @@ -190,7 +190,7 @@ test('llmService handles gemini and cursor MCP JSON config formats', { concurren cwd: './server', }); - await llmService.upsertProviderMcpServer('gemini', { + await llmMcpService.upsertProviderMcpServer('gemini', { name: 'gemini-http', scope: 'project', transport: 'http', @@ -199,7 +199,7 @@ test('llmService handles gemini and cursor MCP JSON config formats', { concurren workspacePath, }); - await llmService.upsertProviderMcpServer('cursor', { + await llmMcpService.upsertProviderMcpServer('cursor', { name: 'cursor-stdio', scope: 'project', transport: 'stdio', @@ -209,7 +209,7 @@ test('llmService handles gemini and cursor MCP JSON config formats', { concurren workspacePath, }); - await llmService.upsertProviderMcpServer('cursor', { + await llmMcpService.upsertProviderMcpServer('cursor', { name: 'cursor-http', scope: 'user', 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 * 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 workspacePath = path.join(tempRoot, 'workspace'); await fs.mkdir(workspacePath, { recursive: true }); const restoreHomeDir = patchHomeDir(tempRoot); try { - const globalResult = await llmService.addMcpServerToAllProviders({ + const globalResult = await llmMcpService.addMcpServerToAllProviders({ name: 'global-http', scope: 'project', transport: 'http', @@ -271,7 +271,7 @@ test('llmService global adder writes to all providers and rejects unsupported tr assert.ok((cursorProject.mcpServers as Record)['global-http']); await assert.rejects( - llmService.addMcpServerToAllProviders({ + llmMcpService.addMcpServerToAllProviders({ name: 'global-sse', scope: 'project', 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. */ -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 workspacePath = path.join(tempRoot, 'workspace'); 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'); const url = `http://127.0.0.1:${address.port}/mcp`; - await llmService.upsertProviderMcpServer('gemini', { + await llmMcpService.upsertProviderMcpServer('gemini', { name: 'probe-http', scope: 'project', transport: 'http', @@ -317,7 +317,7 @@ test('llmService runProviderServer probes stdio and http MCP servers', { concurr workspacePath, }); - await llmService.upsertProviderMcpServer('cursor', { + await llmMcpService.upsertProviderMcpServer('cursor', { name: 'probe-stdio', scope: 'project', transport: 'stdio', @@ -326,7 +326,7 @@ test('llmService runProviderServer probes stdio and http MCP servers', { concurr workspacePath, }); - const httpProbe = await llmService.runProviderMcpServer('gemini', { + const httpProbe = await llmMcpService.runProviderMcpServer('gemini', { name: 'probe-http', scope: 'project', workspacePath, @@ -334,7 +334,7 @@ test('llmService runProviderServer probes stdio and http MCP servers', { concurr assert.equal(httpProbe.reachable, true); assert.equal(httpProbe.transport, 'http'); - const stdioProbe = await llmService.runProviderMcpServer('cursor', { + const stdioProbe = await llmMcpService.runProviderMcpServer('cursor', { name: 'probe-stdio', scope: 'project', workspacePath, @@ -347,3 +347,4 @@ test('llmService runProviderServer probes stdio and http MCP servers', { concurr await fs.rm(tempRoot, { recursive: true, force: true }); } }); + diff --git a/server/src/modules/ai-runtime/tests/skills.test.ts b/server/src/modules/ai-runtime/tests/skills.test.ts index 296764fc..e1b23c29 100644 --- a/server/src/modules/ai-runtime/tests/skills.test.ts +++ b/server/src/modules/ai-runtime/tests/skills.test.ts @@ -4,7 +4,7 @@ import os from 'node:os'; import path from 'node:path'; 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 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. */ -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 workspacePath = path.join(tempRoot, 'workspace'); const pluginInstallPath = path.join(tempRoot, 'plugin-install'); @@ -80,7 +80,7 @@ test('llmService lists claude user/project/plugin skills with proper invocation '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 === 'project' && skill.invocation === '/project-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. */ -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 repoRoot = path.join(tempRoot, 'repo'); 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', }); - 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 === 'parent-skill' && skill.invocation === '$parent-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. */ -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 workspacePath = path.join(tempRoot, 'workspace'); await fs.mkdir(workspacePath, { recursive: true }); @@ -162,7 +162,7 @@ test('llmService lists gemini skills from documented directories', { concurrency 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-agents')); 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. */ -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 workspacePath = path.join(tempRoot, 'workspace'); await fs.mkdir(workspacePath, { recursive: true }); @@ -196,7 +196,7 @@ test('llmService lists cursor skills from documented directories', { concurrency 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-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 }); } }); +