AX Agent ?? ?? ??? MCP ?? ??? ???? ??? ??? ??
- MCP ?? ?????? synthetic skill? ???? McpSkillCatalog? ???? ToolRegistry ?? snapshot ?? ??? ??? - managed/user/additional/project/plugin/mcp/legacy ?? source ??, plugin-only ??, source? inline shell trust boundary? SkillService/AppSettings/Settings UI? ??? - SlashCommandCatalog? ChatWindow?? builtin command? skill? ???? ???? ??? ?? ? ????? dedupe?? MCP ???? ? synthetic skill ?? ??? SkillGallery/AgentSettings? ??? - README.md? docs/DEVELOPMENT.md? 2026-04-14 19:13 (KST) ?? ?? ??? ?? ??? ??? - ??: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase4\\ -p:IntermediateOutputPath=obj\\verify_phase4\\ (?? 0, ?? 0) - ??: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|SlashCommandCatalogTests|McpSkillCatalogTests" -p:OutputPath=bin\\verify_phase4_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4_tests\\ (?? 17, ?? WorkspaceContextGeneratorTests.cs nullable ?? 1? ??)
This commit is contained in:
@@ -7,6 +7,14 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
|||||||
개발 참고: Claw Code 동등성 작업 추적 문서
|
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||||
`docs/claw-code-parity-plan.md`
|
`docs/claw-code-parity-plan.md`
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-14 19:13 (KST)
|
||||||
|
- `claude-code` 기준 Phase 4 범위를 이어서 반영했습니다. MCP 서버 메타데이터를 `mcp` 스코프의 synthetic skill로 노출하는 [McpSkillCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/McpSkillCatalog.cs)를 추가했고, 스킬 source 정책은 `managed/user/additional/project/plugin/mcp/legacy` 단위로 켜고 끌 수 있게 확장했습니다.
|
||||||
|
- 슬래시 명령 합성도 정리했습니다. [SlashCommandCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SlashCommandCatalog.cs)와 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)가 이제 builtin command와 skill을 우선순위 기반으로 dedupe해 같은 `/명령`이 겹칠 때 더 안정적으로 하나만 노출합니다.
|
||||||
|
- 일반 설정과 AX Agent 내부 설정의 스킬 섹션에도 source 정책 토글을 추가했고, [SkillGalleryWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs)는 MCP 카테고리/배지를 지원하도록 보강했습니다. synthetic MCP 스킬처럼 실제 파일이 없는 항목은 편집/복제/내보내기 액션을 숨겨 런타임 오류도 피합니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase4\\ -p:IntermediateOutputPath=obj\\verify_phase4\\` 경고 0 / 오류 0
|
||||||
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|SlashCommandCatalogTests|McpSkillCatalogTests" -p:OutputPath=bin\\verify_phase4_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4_tests\\` 통과 17
|
||||||
|
- 참고: 테스트 프로젝트의 기존 파일 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)` nullable 경고 1건은 유지됩니다.
|
||||||
|
|
||||||
- 업데이트: 2026-04-14 19:16 (KST)
|
- 업데이트: 2026-04-14 19:16 (KST)
|
||||||
- 분석용 로그 저장 방식을 롤링 형태로 정리했습니다. `app`, `perf`, `audit`, `workflow` 로그는 이제 날짜별 파일을 유지하되 각 파일이 최대 1MB를 넘지 않도록 오래된 내용부터 밀어내며 새 로그를 이어 붙입니다.
|
- 분석용 로그 저장 방식을 롤링 형태로 정리했습니다. `app`, `perf`, `audit`, `workflow` 로그는 이제 날짜별 파일을 유지하되 각 파일이 최대 1MB를 넘지 않도록 오래된 내용부터 밀어내며 새 로그를 이어 붙입니다.
|
||||||
- 보관 정책도 같이 정리했습니다. 공통 로그/성능 로그/감사 로그는 14일까지만 유지하고, 워크플로우 상세 로그는 기존 설정값을 따르되 최대 14일을 넘지 않게 제한합니다.
|
- 보관 정책도 같이 정리했습니다. 공통 로그/성능 로그/감사 로그는 14일까지만 유지하고, 워크플로우 상세 로그는 기존 설정값을 따르되 최대 14일을 넘지 않게 제한합니다.
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
# AX Copilot - 개발 문서
|
# AX Copilot - 개발 문서
|
||||||
|
|
||||||
> 최종 업데이트: 2026-04-13 · 버전 0.7.3
|
> 최종 업데이트: 2026-04-14 19:13 (KST) · 버전 0.7.3
|
||||||
|
|
||||||
## 업데이트 로그
|
## 업데이트 로그
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-14 19:13 (KST)
|
||||||
|
- `claude-code` 기준 Phase 4를 이어서 반영했습니다. `src/AxCopilot/Services/Agent/McpSkillCatalog.cs`를 추가해 MCP 서버 메타데이터를 `mcp` source scope의 synthetic skill로 변환하고, `ToolRegistry.RegisterMcpToolsAsync()` 이후 snapshot을 갱신하도록 연결했습니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/SkillService.cs`는 source policy를 `managed/user/additional/project/plugin/mcp/legacy` 단위로 판단하도록 확장했고, source 우선순위 기반 dedupe와 inline shell trust boundary를 함께 적용합니다. plugin-only mode가 켜져 있으면 managed/plugin/bundled만 유지하고 나머지 source는 숨깁니다.
|
||||||
|
- 슬래시 명령 합성은 `src/AxCopilot/Views/SlashCommandCatalog.cs`와 `src/AxCopilot/Views/ChatWindow.xaml.cs`에서 재구성했습니다. builtin command와 skill을 공통 priority로 합성해 충돌 시 한 항목만 노출하고, builtin `/review` 같은 예약 명령이 project skill보다 안정적으로 우선합니다.
|
||||||
|
- 설정/UI는 `src/AxCopilot/Views/SettingsWindow.xaml`, `src/AxCopilot/Views/AgentSettingsWindow.xaml`, `src/AxCopilot/Views/AgentSettingsWindow.xaml.cs`, `src/AxCopilot/Views/SkillGalleryWindow.xaml.cs`에 연결했습니다. MCP 스킬 source 토글, plugin-only mode, source별 inline shell 허용 범위, MCP 카테고리/배지, synthetic skill의 파일 액션 차단을 함께 반영했습니다.
|
||||||
|
- 테스트는 `src/AxCopilot.Tests/Services/SkillServiceRuntimePolicyTests.cs`, `src/AxCopilot.Tests/Services/McpSkillCatalogTests.cs`, `src/AxCopilot.Tests/Views/SlashCommandCatalogTests.cs`에 추가했습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase4\\ -p:IntermediateOutputPath=obj\\verify_phase4\\` 경고 0 / 오류 0
|
||||||
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|SlashCommandCatalogTests|McpSkillCatalogTests" -p:OutputPath=bin\\verify_phase4_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4_tests\\` 통과 17
|
||||||
|
- 참고: 테스트 빌드 중 기존 파일 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)`의 nullable 경고 1건은 유지됩니다.
|
||||||
|
|
||||||
- 업데이트: 2026-04-14 17:46 (KST)
|
- 업데이트: 2026-04-14 17:46 (KST)
|
||||||
- 도구 이름 정합성 문제를 줄이기 위해 `src/AxCopilot/Services/Agent/AgentToolCatalog.cs`를 추가했습니다. canonical id, legacy alias, 탭 노출, 설정 카테고리, 병렬 read-only 분류를 한곳에서 관리하도록 정리했습니다.
|
- 도구 이름 정합성 문제를 줄이기 위해 `src/AxCopilot/Services/Agent/AgentToolCatalog.cs`를 추가했습니다. canonical id, legacy alias, 탭 노출, 설정 카테고리, 병렬 read-only 분류를 한곳에서 관리하도록 정리했습니다.
|
||||||
- `ToolRegistry`, `AgentLoopService`, `AgentLoopParallelExecution`, `IAgentTool`, `AgentHookRunner`, `SkillService`가 모두 같은 카탈로그를 사용하도록 연결했습니다. 이에 따라 `git/lsp/zip/project_rule/snippet_run` 같은 예전 이름도 런타임에서 자동 정규화됩니다.
|
- `ToolRegistry`, `AgentLoopService`, `AgentLoopParallelExecution`, `IAgentTool`, `AgentHookRunner`, `SkillService`가 모두 같은 카탈로그를 사용하도록 연결했습니다. 이에 따라 `git/lsp/zip/project_rule/snippet_run` 같은 예전 이름도 런타임에서 자동 정규화됩니다.
|
||||||
|
|||||||
59
src/AxCopilot.Tests/Services/McpSkillCatalogTests.cs
Normal file
59
src/AxCopilot.Tests/Services/McpSkillCatalogTests.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AxCopilot.Tests.Services;
|
||||||
|
|
||||||
|
public class McpSkillCatalogTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BuildSyntheticSkills_ShouldCreateMcpScopedSkill_ForEnabledServer()
|
||||||
|
{
|
||||||
|
var skills = McpSkillCatalog.BuildSyntheticSkills(
|
||||||
|
[
|
||||||
|
new McpServerEntry
|
||||||
|
{
|
||||||
|
Name = "chrome-devtools",
|
||||||
|
Enabled = true,
|
||||||
|
Transport = "stdio",
|
||||||
|
Command = "npx"
|
||||||
|
},
|
||||||
|
new McpServerEntry
|
||||||
|
{
|
||||||
|
Name = "disabled-server",
|
||||||
|
Enabled = false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
skills.Should().ContainSingle();
|
||||||
|
var skill = skills[0];
|
||||||
|
skill.Name.Should().Be("mcp:chrome-devtools");
|
||||||
|
skill.SourceScope.Should().Be("mcp");
|
||||||
|
skill.Label.Should().Be("chrome-devtools MCP");
|
||||||
|
skill.AllowedTools.Should().Contain("mcp_list_resources");
|
||||||
|
skill.AllowedTools.Should().Contain("mcp_read_resource");
|
||||||
|
skill.SystemPrompt.Should().Contain("Use the MCP server");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComputeSignature_ShouldReflectEnabledServerConfiguration()
|
||||||
|
{
|
||||||
|
var servers = new[]
|
||||||
|
{
|
||||||
|
new McpServerEntry
|
||||||
|
{
|
||||||
|
Name = "docs",
|
||||||
|
Enabled = true,
|
||||||
|
Transport = "sse",
|
||||||
|
Url = "https://intra.example.local/mcp/sse"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var signature = McpSkillCatalog.ComputeSignature(servers);
|
||||||
|
|
||||||
|
signature.Should().Contain("docs");
|
||||||
|
signature.Should().Contain("sse");
|
||||||
|
signature.Should().Contain("https://intra.example.local/mcp/sse");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using AxCopilot.Services.Agent;
|
using AxCopilot.Services.Agent;
|
||||||
|
using AxCopilot.Models;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using System;
|
using System;
|
||||||
@@ -303,4 +304,60 @@ public class SkillServiceRuntimePolicyTests
|
|||||||
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsSkillSourceEnabled_ShouldRespectSourceFlags_AndPluginOnlyMode()
|
||||||
|
{
|
||||||
|
var llm = new LlmSettings
|
||||||
|
{
|
||||||
|
EnableManagedSkillSource = true,
|
||||||
|
EnableUserSkillSource = false,
|
||||||
|
EnableAdditionalSkillDiscovery = false,
|
||||||
|
EnableProjectSkillDiscovery = true,
|
||||||
|
EnablePluginSkillDiscovery = true,
|
||||||
|
EnableMcpSkillDiscovery = false,
|
||||||
|
EnableLegacyCommandSkills = false,
|
||||||
|
EnablePluginOnlySkillMode = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
SkillService.IsSkillSourceEnabled("managed", llm).Should().BeTrue();
|
||||||
|
SkillService.IsSkillSourceEnabled("user", llm).Should().BeFalse();
|
||||||
|
SkillService.IsSkillSourceEnabled("additional", llm).Should().BeFalse();
|
||||||
|
SkillService.IsSkillSourceEnabled("project", llm).Should().BeTrue();
|
||||||
|
SkillService.IsSkillSourceEnabled("mcp", llm).Should().BeFalse();
|
||||||
|
SkillService.IsSkillSourceEnabled("legacy", llm).Should().BeFalse();
|
||||||
|
|
||||||
|
llm.EnablePluginOnlySkillMode = true;
|
||||||
|
|
||||||
|
SkillService.IsSkillSourceEnabled("managed", llm).Should().BeTrue();
|
||||||
|
SkillService.IsSkillSourceEnabled("plugin", llm).Should().BeTrue();
|
||||||
|
SkillService.IsSkillSourceEnabled("project", llm).Should().BeFalse();
|
||||||
|
SkillService.IsSkillSourceEnabled("mcp", llm).Should().BeFalse();
|
||||||
|
SkillService.IsSkillSourceEnabled("bundled", llm).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetInlineShellBlockedReason_ShouldRespectSourceTrustBoundaries()
|
||||||
|
{
|
||||||
|
var llm = new LlmSettings
|
||||||
|
{
|
||||||
|
EnableSkillInlineShell = true,
|
||||||
|
AllowPluginSkillInlineShell = false,
|
||||||
|
AllowMcpSkillInlineShell = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
var pluginSkill = new SkillDefinition { Name = "plugin-a", SourceScope = "plugin" };
|
||||||
|
var mcpSkill = new SkillDefinition { Name = "mcp-a", SourceScope = "mcp" };
|
||||||
|
var projectSkill = new SkillDefinition { Name = "project-a", SourceScope = "project" };
|
||||||
|
|
||||||
|
SkillService.GetInlineShellBlockedReason(pluginSkill, llm).Should().Be("[inline-shell blocked for plugin skills]");
|
||||||
|
SkillService.GetInlineShellBlockedReason(mcpSkill, llm).Should().Be("[inline-shell blocked for mcp skills]");
|
||||||
|
SkillService.GetInlineShellBlockedReason(projectSkill, llm).Should().BeNull();
|
||||||
|
|
||||||
|
llm.AllowPluginSkillInlineShell = true;
|
||||||
|
llm.AllowMcpSkillInlineShell = true;
|
||||||
|
|
||||||
|
SkillService.GetInlineShellBlockedReason(pluginSkill, llm).Should().BeNull();
|
||||||
|
SkillService.GetInlineShellBlockedReason(mcpSkill, llm).Should().BeNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,4 +28,25 @@ public class SlashCommandCatalogTests
|
|||||||
SlashCommandCatalog.TryGetEntry("/mcp", out var mcpEntry).Should().BeTrue();
|
SlashCommandCatalog.TryGetEntry("/mcp", out var mcpEntry).Should().BeTrue();
|
||||||
mcpEntry.SystemPrompt.Should().Be("__MCP__");
|
mcpEntry.SystemPrompt.Should().Be("__MCP__");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComposeMatches_ShouldPreferHigherPriorityEntry_WhenCommandCollides()
|
||||||
|
{
|
||||||
|
var matches = SlashCommandCatalog.ComposeMatches(
|
||||||
|
[
|
||||||
|
("/review", "Project Review Skill", true, 900),
|
||||||
|
("/review", "Code Review", false, 2100),
|
||||||
|
("/verify-change", "Verify Skill", true, 600)
|
||||||
|
]);
|
||||||
|
|
||||||
|
matches.Should().ContainSingle(x => x.Cmd == "/review");
|
||||||
|
matches.Should().Contain(x => x.Cmd == "/review" && x.IsSkill == false && x.Label == "Code Review");
|
||||||
|
matches.Should().Contain(x => x.Cmd == "/verify-change" && x.IsSkill);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetBuiltInCommandPriority_ShouldGiveDevCommandsHigherPriority()
|
||||||
|
{
|
||||||
|
SlashCommandCatalog.GetBuiltInCommandPriority("/review").Should().BeGreaterThan(SlashCommandCatalog.GetBuiltInCommandPriority("/clear"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1296,18 +1296,39 @@ public class LlmSettings
|
|||||||
[JsonPropertyName("additionalSkillFolders")]
|
[JsonPropertyName("additionalSkillFolders")]
|
||||||
public List<string> AdditionalSkillFolders { get; set; } = new();
|
public List<string> AdditionalSkillFolders { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("enableManagedSkillSource")]
|
||||||
|
public bool EnableManagedSkillSource { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonPropertyName("enableUserSkillSource")]
|
||||||
|
public bool EnableUserSkillSource { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonPropertyName("enableAdditionalSkillDiscovery")]
|
||||||
|
public bool EnableAdditionalSkillDiscovery { get; set; } = true;
|
||||||
|
|
||||||
[JsonPropertyName("enableProjectSkillDiscovery")]
|
[JsonPropertyName("enableProjectSkillDiscovery")]
|
||||||
public bool EnableProjectSkillDiscovery { get; set; } = true;
|
public bool EnableProjectSkillDiscovery { get; set; } = true;
|
||||||
|
|
||||||
[JsonPropertyName("enablePluginSkillDiscovery")]
|
[JsonPropertyName("enablePluginSkillDiscovery")]
|
||||||
public bool EnablePluginSkillDiscovery { get; set; } = true;
|
public bool EnablePluginSkillDiscovery { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonPropertyName("enableMcpSkillDiscovery")]
|
||||||
|
public bool EnableMcpSkillDiscovery { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonPropertyName("enablePluginOnlySkillMode")]
|
||||||
|
public bool EnablePluginOnlySkillMode { get; set; } = false;
|
||||||
|
|
||||||
[JsonPropertyName("enableLegacyCommandSkills")]
|
[JsonPropertyName("enableLegacyCommandSkills")]
|
||||||
public bool EnableLegacyCommandSkills { get; set; } = true;
|
public bool EnableLegacyCommandSkills { get; set; } = true;
|
||||||
|
|
||||||
[JsonPropertyName("enableSkillInlineShell")]
|
[JsonPropertyName("enableSkillInlineShell")]
|
||||||
public bool EnableSkillInlineShell { get; set; } = true;
|
public bool EnableSkillInlineShell { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonPropertyName("allowPluginSkillInlineShell")]
|
||||||
|
public bool AllowPluginSkillInlineShell { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("allowMcpSkillInlineShell")]
|
||||||
|
public bool AllowMcpSkillInlineShell { get; set; } = false;
|
||||||
|
|
||||||
[JsonPropertyName("skillInlineShellTimeoutSeconds")]
|
[JsonPropertyName("skillInlineShellTimeoutSeconds")]
|
||||||
public int SkillInlineShellTimeoutSeconds { get; set; } = 8;
|
public int SkillInlineShellTimeoutSeconds { get; set; } = 8;
|
||||||
|
|
||||||
|
|||||||
257
src/AxCopilot/Services/Agent/McpSkillCatalog.cs
Normal file
257
src/AxCopilot/Services/Agent/McpSkillCatalog.cs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
internal static class McpSkillCatalog
|
||||||
|
{
|
||||||
|
private static readonly object s_gate = new();
|
||||||
|
private static readonly Dictionary<string, McpServerSnapshot> s_snapshots = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static void RefreshFromClients(IEnumerable<McpClientService> clients)
|
||||||
|
{
|
||||||
|
if (clients == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (s_gate)
|
||||||
|
{
|
||||||
|
foreach (var client in clients)
|
||||||
|
{
|
||||||
|
if (client == null || string.IsNullOrWhiteSpace(client.ServerName))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var serverName = client.ServerName.Trim();
|
||||||
|
s_snapshots[serverName] = new McpServerSnapshot(
|
||||||
|
serverName,
|
||||||
|
client.Tools
|
||||||
|
.Select(tool => new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = tool.Name,
|
||||||
|
Description = tool.Description,
|
||||||
|
ServerName = tool.ServerName,
|
||||||
|
Parameters = tool.Parameters.ToDictionary(
|
||||||
|
pair => pair.Key,
|
||||||
|
pair => new McpParameterDef
|
||||||
|
{
|
||||||
|
Type = pair.Value.Type,
|
||||||
|
Description = pair.Value.Description,
|
||||||
|
Required = pair.Value.Required,
|
||||||
|
},
|
||||||
|
StringComparer.OrdinalIgnoreCase),
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly(),
|
||||||
|
client.Resources
|
||||||
|
.Select(resource => new McpResourceDefinition
|
||||||
|
{
|
||||||
|
Uri = resource.Uri,
|
||||||
|
Name = resource.Name,
|
||||||
|
Description = resource.Description,
|
||||||
|
MimeType = resource.MimeType,
|
||||||
|
ServerName = resource.ServerName,
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly(),
|
||||||
|
DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<SkillDefinition> BuildSyntheticSkills(IEnumerable<McpServerEntry>? servers)
|
||||||
|
{
|
||||||
|
if (servers == null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var snapshots = GetSnapshotMap();
|
||||||
|
var skills = new List<SkillDefinition>();
|
||||||
|
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var server in servers
|
||||||
|
.Where(server => server != null && server.Enabled)
|
||||||
|
.OrderBy(server => server.Name, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var displayName = string.IsNullOrWhiteSpace(server.Name) ? "mcp-server" : server.Name.Trim();
|
||||||
|
var skillName = BuildUniqueSkillName(displayName, usedNames);
|
||||||
|
snapshots.TryGetValue(displayName, out var snapshot);
|
||||||
|
|
||||||
|
var allowedTools = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"mcp_list_resources",
|
||||||
|
"mcp_read_resource"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (snapshot != null)
|
||||||
|
{
|
||||||
|
foreach (var tool in snapshot.Tools)
|
||||||
|
allowedTools.Add($"mcp_{tool.ServerName}_{tool.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
skills.Add(new SkillDefinition
|
||||||
|
{
|
||||||
|
Id = skillName,
|
||||||
|
Name = skillName,
|
||||||
|
Label = $"{displayName} MCP",
|
||||||
|
Description = BuildDescription(displayName, snapshot),
|
||||||
|
SystemPrompt = BuildPromptBody(displayName, server, snapshot),
|
||||||
|
FilePath = $"[mcp]/{displayName}",
|
||||||
|
SkillRoot = "",
|
||||||
|
SourceScope = "mcp",
|
||||||
|
AllowedTools = string.Join(", ", allowedTools.OrderBy(tool => tool, StringComparer.OrdinalIgnoreCase)),
|
||||||
|
WhenToUse = BuildWhenToUse(displayName, snapshot),
|
||||||
|
Tabs = "all",
|
||||||
|
UserInvocable = true,
|
||||||
|
IsAvailable = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return skills;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ComputeSignature(IEnumerable<McpServerEntry>? servers)
|
||||||
|
{
|
||||||
|
var snapshots = GetSnapshotMap();
|
||||||
|
var parts = new List<string>();
|
||||||
|
var orderedServers = (servers ?? [])
|
||||||
|
.Where(server => server != null)
|
||||||
|
.OrderBy(server => server.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var server in orderedServers)
|
||||||
|
{
|
||||||
|
var name = string.IsNullOrWhiteSpace(server.Name) ? "(unnamed)" : server.Name.Trim();
|
||||||
|
snapshots.TryGetValue(name, out var snapshot);
|
||||||
|
parts.Add(string.Join(":",
|
||||||
|
name,
|
||||||
|
server.Enabled ? "1" : "0",
|
||||||
|
server.Transport ?? "stdio",
|
||||||
|
server.Command ?? "",
|
||||||
|
server.Url ?? "",
|
||||||
|
snapshot?.Tools.Count ?? 0,
|
||||||
|
snapshot?.Resources.Count ?? 0,
|
||||||
|
snapshot?.UpdatedAtUtc.Ticks ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("|", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, McpServerSnapshot> GetSnapshotMap()
|
||||||
|
{
|
||||||
|
lock (s_gate)
|
||||||
|
return new Dictionary<string, McpServerSnapshot>(s_snapshots, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildDescription(string displayName, McpServerSnapshot? snapshot)
|
||||||
|
{
|
||||||
|
if (snapshot == null)
|
||||||
|
return $"{displayName} MCP 서버의 도구와 리소스를 탐색하는 보조 스킬입니다.";
|
||||||
|
|
||||||
|
var toolCount = snapshot.Tools.Count;
|
||||||
|
var resourceCount = snapshot.Resources.Count;
|
||||||
|
if (toolCount == 0 && resourceCount == 0)
|
||||||
|
return $"{displayName} MCP 서버에 연결되었지만 아직 노출된 도구나 리소스가 없습니다.";
|
||||||
|
|
||||||
|
return $"{displayName} MCP 서버의 도구 {toolCount}개와 리소스 {resourceCount}개를 우선 활용하도록 돕습니다.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildWhenToUse(string displayName, McpServerSnapshot? snapshot)
|
||||||
|
{
|
||||||
|
if (snapshot == null || (snapshot.Tools.Count == 0 && snapshot.Resources.Count == 0))
|
||||||
|
return $"{displayName} MCP 서버가 제공하는 리소스나 도구를 먼저 확인해야 하는 작업";
|
||||||
|
|
||||||
|
var keywords = snapshot.Resources
|
||||||
|
.SelectMany(resource => new[] { resource.Name, resource.Description, resource.MimeType })
|
||||||
|
.Concat(snapshot.Tools.SelectMany(tool => new[] { tool.Name, tool.Description }))
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||||
|
.Select(value => value!.Trim())
|
||||||
|
.Take(4)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return keywords.Count == 0
|
||||||
|
? $"{displayName} MCP 서버의 도구와 리소스가 필요한 작업"
|
||||||
|
: $"{displayName} MCP 서버의 {string.Join(", ", keywords)} 관련 정보를 찾거나 실행해야 하는 작업";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildPromptBody(string displayName, McpServerEntry server, McpServerSnapshot? snapshot)
|
||||||
|
{
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
$"Use the MCP server \"{displayName}\" as the first source of truth when it is relevant.",
|
||||||
|
$"Transport: {NormalizeTransport(server.Transport)}",
|
||||||
|
$"1. Start with `mcp_list_resources(server_name: \"{displayName}\")` to discover available resources.",
|
||||||
|
"2. Read only the specific resources you need with `mcp_read_resource`.",
|
||||||
|
"3. If server-specific MCP tools are available, prefer the smallest relevant call before broader exploration.",
|
||||||
|
"4. Summarize the MCP evidence you used and clearly note any missing data."
|
||||||
|
};
|
||||||
|
|
||||||
|
if (snapshot == null)
|
||||||
|
{
|
||||||
|
lines.Add(string.Empty);
|
||||||
|
lines.Add("This session does not yet have a cached MCP metadata snapshot, so inspect the live resource list first.");
|
||||||
|
return string.Join(Environment.NewLine, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.Tools.Count > 0)
|
||||||
|
{
|
||||||
|
lines.Add(string.Empty);
|
||||||
|
lines.Add("Available server-specific MCP tools:");
|
||||||
|
foreach (var tool in snapshot.Tools.Take(8))
|
||||||
|
{
|
||||||
|
var description = string.IsNullOrWhiteSpace(tool.Description) ? "" : $" - {tool.Description}";
|
||||||
|
lines.Add($"- mcp_{tool.ServerName}_{tool.Name}{description}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.Resources.Count > 0)
|
||||||
|
{
|
||||||
|
lines.Add(string.Empty);
|
||||||
|
lines.Add("Known MCP resources:");
|
||||||
|
foreach (var resource in snapshot.Resources.Take(8))
|
||||||
|
{
|
||||||
|
var descriptor = new StringBuilder();
|
||||||
|
descriptor.Append($"- {resource.Name}");
|
||||||
|
if (!string.IsNullOrWhiteSpace(resource.MimeType))
|
||||||
|
descriptor.Append($" ({resource.MimeType})");
|
||||||
|
if (!string.IsNullOrWhiteSpace(resource.Description))
|
||||||
|
descriptor.Append($" - {resource.Description}");
|
||||||
|
lines.Add(descriptor.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(Environment.NewLine, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildUniqueSkillName(string displayName, ISet<string> usedNames)
|
||||||
|
{
|
||||||
|
var baseName = $"mcp:{NormalizeToken(displayName)}";
|
||||||
|
var current = baseName;
|
||||||
|
var suffix = 2;
|
||||||
|
while (!usedNames.Add(current))
|
||||||
|
{
|
||||||
|
current = $"{baseName}-{suffix}";
|
||||||
|
suffix++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeToken(string input)
|
||||||
|
{
|
||||||
|
var normalized = Regex.Replace((input ?? "").Trim().ToLowerInvariant(), @"[^a-z0-9]+", "-").Trim('-');
|
||||||
|
return string.IsNullOrWhiteSpace(normalized) ? "server" : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeTransport(string? transport)
|
||||||
|
{
|
||||||
|
return (transport ?? "stdio").Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"sse" => "server-sent events",
|
||||||
|
_ => "stdio",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record McpServerSnapshot(
|
||||||
|
string ServerName,
|
||||||
|
IReadOnlyList<McpToolDefinition> Tools,
|
||||||
|
IReadOnlyList<McpResourceDefinition> Resources,
|
||||||
|
DateTime UpdatedAtUtc);
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ public class SkillManagerTool : IAgentTool
|
|||||||
{
|
{
|
||||||
var skills = SkillService.Skills;
|
var skills = SkillService.Skills;
|
||||||
if (skills.Count == 0)
|
if (skills.Count == 0)
|
||||||
return ToolResult.Ok("로드된 스킬이 없습니다. 번들 스킬, %APPDATA%\\AxCopilot\\skills\\, 추가 스킬 폴더, 프로젝트 .claude\\skills\\를 확인하세요.");
|
return ToolResult.Ok("로드된 스킬이 없습니다. 번들 스킬, %APPDATA%\\AxCopilot\\skills\\, 추가 스킬 폴더, 프로젝트 .claude\\skills\\, MCP 스킬 소스를 확인하세요.");
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine($"사용 가능한 스킬 ({skills.Count}개):\n");
|
sb.AppendLine($"사용 가능한 스킬 ({skills.Count}개):\n");
|
||||||
|
|||||||
@@ -33,18 +33,25 @@ public static class SkillService
|
|||||||
/// <summary>스킬 폴더에서 *.skill.md / SKILL.md 파일을 로드합니다.</summary>
|
/// <summary>스킬 폴더에서 *.skill.md / SKILL.md 파일을 로드합니다.</summary>
|
||||||
public static void LoadSkills(string? customFolder = null, string? projectRoot = null, IEnumerable<string>? additionalFolders = null)
|
public static void LoadSkills(string? customFolder = null, string? projectRoot = null, IEnumerable<string>? additionalFolders = null)
|
||||||
{
|
{
|
||||||
|
var app = System.Windows.Application.Current as App;
|
||||||
|
var llm = app?.SettingsService?.Settings.Llm;
|
||||||
var normalizedCustomFolder = NormalizeExistingDirectory(customFolder);
|
var normalizedCustomFolder = NormalizeExistingDirectory(customFolder);
|
||||||
var normalizedProjectRoot = NormalizeExistingDirectory(projectRoot);
|
var normalizedProjectRoot = NormalizeExistingDirectory(projectRoot);
|
||||||
var normalizedAdditionalFolders = NormalizeDistinctDirectories(additionalFolders);
|
var normalizedAdditionalFolders = NormalizeDistinctDirectories(additionalFolders);
|
||||||
var sources = BuildSkillSources(normalizedCustomFolder, normalizedProjectRoot, normalizedAdditionalFolders).ToList();
|
var sources = BuildSkillSources(normalizedCustomFolder, normalizedProjectRoot, normalizedAdditionalFolders).ToList();
|
||||||
var loadSignature = ComputeLoadSignature(sources);
|
var mcpSkills = BuildMcpSkills(llm?.McpServers, llm);
|
||||||
|
var loadSignature = ComputeLoadSignature(sources, llm, mcpSkills);
|
||||||
if (_skills.Count > 0 && string.Equals(_lastLoadSignature, loadSignature, StringComparison.OrdinalIgnoreCase))
|
if (_skills.Count > 0 && string.Equals(_lastLoadSignature, loadSignature, StringComparison.OrdinalIgnoreCase))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var allSkills = GetBundledSkills().ToList();
|
var candidates = new List<SkillLoadCandidate>();
|
||||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var order = 0;
|
||||||
foreach (var bundled in allSkills)
|
|
||||||
seen.Add(bundled.Name);
|
foreach (var bundled in GetBundledSkills())
|
||||||
|
candidates.Add(new SkillLoadCandidate(order++, bundled));
|
||||||
|
|
||||||
|
foreach (var mcpSkill in mcpSkills)
|
||||||
|
candidates.Add(new SkillLoadCandidate(order++, mcpSkill));
|
||||||
|
|
||||||
foreach (var source in sources)
|
foreach (var source in sources)
|
||||||
{
|
{
|
||||||
@@ -57,8 +64,8 @@ public static class SkillService
|
|||||||
var skill = source.Kind == SkillSourceKind.LegacyCommand
|
var skill = source.Kind == SkillSourceKind.LegacyCommand
|
||||||
? ParseLegacyCommandFile(file, source.Directory, source.Scope)
|
? ParseLegacyCommandFile(file, source.Directory, source.Scope)
|
||||||
: ParseSkillFile(file, source.Directory, source.Scope);
|
: ParseSkillFile(file, source.Directory, source.Scope);
|
||||||
if (skill != null && seen.Add(skill.Name))
|
if (skill != null)
|
||||||
allSkills.Add(skill);
|
candidates.Add(new SkillLoadCandidate(order++, skill));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -67,6 +74,17 @@ public static class SkillService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var allSkills = candidates
|
||||||
|
.GroupBy(candidate => candidate.Skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => group
|
||||||
|
.OrderByDescending(candidate => GetSkillSourcePriority(candidate.Skill.SourceScope))
|
||||||
|
.ThenBy(candidate => candidate.LoadOrder)
|
||||||
|
.Select(candidate => candidate.Skill)
|
||||||
|
.First())
|
||||||
|
.OrderByDescending(skill => GetSkillSourcePriority(skill.SourceScope))
|
||||||
|
.ThenBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// 런타임 의존성 검증
|
// 런타임 의존성 검증
|
||||||
foreach (var skill in allSkills)
|
foreach (var skill in allSkills)
|
||||||
{
|
{
|
||||||
@@ -338,6 +356,8 @@ public static class SkillService
|
|||||||
|
|
||||||
void AddSource(string? directory, string scope, SkillSourceKind kind = SkillSourceKind.SkillFolder)
|
void AddSource(string? directory, string scope, SkillSourceKind kind = SkillSourceKind.SkillFolder)
|
||||||
{
|
{
|
||||||
|
if (!IsSkillSourceEnabled(scope, llm))
|
||||||
|
return;
|
||||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -350,8 +370,11 @@ public static class SkillService
|
|||||||
AddSource(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"), "user");
|
AddSource(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"), "user");
|
||||||
AddSource(customFolder, "custom");
|
AddSource(customFolder, "custom");
|
||||||
|
|
||||||
foreach (var folder in additionalFolders)
|
if (llm?.EnableAdditionalSkillDiscovery ?? true)
|
||||||
AddSource(folder, "additional");
|
{
|
||||||
|
foreach (var folder in additionalFolders)
|
||||||
|
AddSource(folder, "additional");
|
||||||
|
}
|
||||||
|
|
||||||
if (llm?.EnablePluginSkillDiscovery ?? true)
|
if (llm?.EnablePluginSkillDiscovery ?? true)
|
||||||
{
|
{
|
||||||
@@ -374,7 +397,10 @@ public static class SkillService
|
|||||||
return sources;
|
return sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ComputeLoadSignature(IEnumerable<SkillSourceDescriptor> sources)
|
private static string ComputeLoadSignature(
|
||||||
|
IEnumerable<SkillSourceDescriptor> sources,
|
||||||
|
Models.LlmSettings? llm,
|
||||||
|
IReadOnlyList<SkillDefinition> mcpSkills)
|
||||||
{
|
{
|
||||||
var parts = new List<string>();
|
var parts = new List<string>();
|
||||||
foreach (var source in sources)
|
foreach (var source in sources)
|
||||||
@@ -388,9 +414,68 @@ public static class SkillService
|
|||||||
parts.Add($"{source.Kind}:{source.Scope}:{source.Directory}:{files.Count}:{latestWrite}");
|
parts.Add($"{source.Kind}:{source.Scope}:{source.Directory}:{files.Count}:{latestWrite}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parts.Add($"skill-policy:{llm?.EnableManagedSkillSource ?? true}:{llm?.EnableUserSkillSource ?? true}:{llm?.EnableAdditionalSkillDiscovery ?? true}:{llm?.EnableProjectSkillDiscovery ?? true}:{llm?.EnablePluginSkillDiscovery ?? true}:{llm?.EnableMcpSkillDiscovery ?? true}:{llm?.EnablePluginOnlySkillMode ?? false}:{llm?.EnableLegacyCommandSkills ?? true}");
|
||||||
|
parts.Add($"mcp:{McpSkillCatalog.ComputeSignature(llm?.McpServers)}:{mcpSkills.Count}");
|
||||||
return string.Join("|", parts);
|
return string.Join("|", parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<SkillDefinition> BuildMcpSkills(
|
||||||
|
IEnumerable<Models.McpServerEntry>? servers,
|
||||||
|
Models.LlmSettings? llm)
|
||||||
|
{
|
||||||
|
if (!IsSkillSourceEnabled("mcp", llm))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return McpSkillCatalog.BuildSyntheticSkills(servers);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsSkillSourceEnabled(string scope, Models.LlmSettings? llm)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(scope))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var normalized = scope.Trim().ToLowerInvariant();
|
||||||
|
if (normalized == "bundled")
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (llm == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (llm.EnablePluginOnlySkillMode)
|
||||||
|
{
|
||||||
|
return normalized is "managed" or "plugin";
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"managed" => llm.EnableManagedSkillSource,
|
||||||
|
"user" or "custom" => llm.EnableUserSkillSource,
|
||||||
|
"additional" => llm.EnableAdditionalSkillDiscovery,
|
||||||
|
"project" => llm.EnableProjectSkillDiscovery,
|
||||||
|
"plugin" => llm.EnablePluginSkillDiscovery,
|
||||||
|
"mcp" => llm.EnableMcpSkillDiscovery,
|
||||||
|
"legacy" => llm.EnableLegacyCommandSkills,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int GetSkillSourcePriority(string? sourceScope)
|
||||||
|
{
|
||||||
|
return (sourceScope ?? "").Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"project" => 900,
|
||||||
|
"mcp" => 850,
|
||||||
|
"plugin" => 800,
|
||||||
|
"additional" => 760,
|
||||||
|
"custom" => 740,
|
||||||
|
"user" => 700,
|
||||||
|
"managed" => 650,
|
||||||
|
"bundled" => 600,
|
||||||
|
"legacy" => 550,
|
||||||
|
_ => 500,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> EnumerateProjectSkillFolders(string? projectRoot)
|
private static IEnumerable<string> EnumerateProjectSkillFolders(string? projectRoot)
|
||||||
{
|
{
|
||||||
foreach (var root in EnumerateAncestorDirectories(projectRoot))
|
foreach (var root in EnumerateAncestorDirectories(projectRoot))
|
||||||
@@ -527,7 +612,57 @@ public static class SkillService
|
|||||||
완료한 작업, 남은 이슈, 주의할 파일, 다음 권장 액션을 분리해서 작성하세요.
|
완료한 작업, 남은 이슈, 주의할 파일, 다음 권장 액션을 분리해서 작성하세요.
|
||||||
""",
|
""",
|
||||||
whenToUse: "작업 마무리, 인수인계, 세션 요약, 다음 액션 정리가 필요할 때",
|
whenToUse: "작업 마무리, 인수인계, 세션 요약, 다음 액션 정리가 필요할 때",
|
||||||
allowedTools: "git_tool, file_read, task_list")
|
allowedTools: "git_tool, file_read, task_list"),
|
||||||
|
CreateBundledSkill(
|
||||||
|
"stuck-recovery",
|
||||||
|
"막힘 복구",
|
||||||
|
"막힌 작업을 다시 전진시키기 위한 원인 정리와 우회 경로를 제안합니다.",
|
||||||
|
"""
|
||||||
|
현재 작업이 멈춘 이유를 입력, 환경, 도구, 정책, 검증 결과 관점에서 나눠 분석하세요.
|
||||||
|
즉시 시도할 수 있는 우회 경로와, 사용자의 결정을 받아야 하는 항목을 분리해 정리하세요.
|
||||||
|
""",
|
||||||
|
whenToUse: "반복 실패, 권한 차단, 환경 문제, 막힘 복구가 필요한 작업",
|
||||||
|
allowedTools: "file_read, grep, build_run, process, task_list"),
|
||||||
|
CreateBundledSkill(
|
||||||
|
"context-memory",
|
||||||
|
"컨텍스트 메모리",
|
||||||
|
"지속적으로 참고해야 할 사실과 후속 체크포인트를 짧게 정리합니다.",
|
||||||
|
"""
|
||||||
|
현재 작업에서 다음 반복이나 다음 세션에도 유지해야 할 핵심 사실만 추려 정리하세요.
|
||||||
|
파일 경로, 결정 사항, 미해결 리스크, 다음 확인 지점을 짧은 메모 형식으로 요약하세요.
|
||||||
|
""",
|
||||||
|
whenToUse: "세션이 길어지거나, 다음 반복을 위해 핵심 사실을 압축해 남겨야 할 때",
|
||||||
|
allowedTools: "memory, file_read, task_list"),
|
||||||
|
CreateBundledSkill(
|
||||||
|
"config-tune",
|
||||||
|
"설정 튜닝",
|
||||||
|
"현재 설정/정책 상태를 바탕으로 실행 품질과 안전성을 조정합니다.",
|
||||||
|
"""
|
||||||
|
현재 작업 목적에 맞춰 설정을 점검하고, 변경이 필요한 항목과 그대로 둬야 하는 항목을 구분하세요.
|
||||||
|
성능, 권한, 스킬 source, 도구 노출 관점에서 추천 변경안을 우선순위로 제시하세요.
|
||||||
|
""",
|
||||||
|
whenToUse: "권한, 스킬 source, 도구 노출, 자동화 설정을 조정해야 할 때",
|
||||||
|
allowedTools: "skill_manager, env_tool, mcp_list_resources"),
|
||||||
|
CreateBundledSkill(
|
||||||
|
"change-guard",
|
||||||
|
"변경 가드",
|
||||||
|
"수정 전에 영향 범위와 회귀 가능성을 빠르게 짚어줍니다.",
|
||||||
|
"""
|
||||||
|
변경 대상의 영향 범위, 연관 파일, 테스트 누락 가능성을 먼저 정리하세요.
|
||||||
|
실제 수정 전에 어떤 확인이 필요한지와 수정 후 어떤 검증을 해야 하는지를 함께 제시하세요.
|
||||||
|
""",
|
||||||
|
whenToUse: "수정 전에 영향 범위와 회귀 리스크를 빠르게 훑어야 할 때",
|
||||||
|
allowedTools: "file_read, grep, lsp_code_intel, folder_map"),
|
||||||
|
CreateBundledSkill(
|
||||||
|
"delivery-pack",
|
||||||
|
"전달 패키지",
|
||||||
|
"배포/공유 직전 필요한 결과물과 커뮤니케이션 패키지를 정리합니다.",
|
||||||
|
"""
|
||||||
|
배포 또는 공유 직전에 필요한 요약, 변경 포인트, 검증 상태, 사용자 전달 문구를 정리하세요.
|
||||||
|
문서, PPT, 보고서가 함께 필요한 경우 어떤 산출물을 어떤 순서로 만들지 제안하세요.
|
||||||
|
""",
|
||||||
|
whenToUse: "배포 직전, 보고 직전, 문서/PPT와 함께 전달 패키지를 정리해야 할 때",
|
||||||
|
allowedTools: "document_plan, document_assemble, pptx_create, docx_create, markdown_create")
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1172,13 +1307,7 @@ public static class SkillService
|
|||||||
score += 4;
|
score += 4;
|
||||||
if (!skill.UserInvocable)
|
if (!skill.UserInvocable)
|
||||||
score += 1;
|
score += 1;
|
||||||
score += skill.SourceScope switch
|
score += Math.Max(0, GetSkillSourcePriority(skill.SourceScope) / 200);
|
||||||
{
|
|
||||||
"project" => 3,
|
|
||||||
"plugin" => 2,
|
|
||||||
"bundled" => 1,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
if (skill.DisableModelInvocation)
|
if (skill.DisableModelInvocation)
|
||||||
score += 1;
|
score += 1;
|
||||||
|
|
||||||
@@ -1338,11 +1467,14 @@ public static class SkillService
|
|||||||
return input;
|
return input;
|
||||||
|
|
||||||
var result = input;
|
var result = input;
|
||||||
|
var app = System.Windows.Application.Current as App;
|
||||||
|
var llm = app?.SettingsService?.Settings.Llm;
|
||||||
|
var blockedReason = GetInlineShellBlockedReason(skill, llm);
|
||||||
foreach (Match match in matches.Cast<Match>().Reverse())
|
foreach (Match match in matches.Cast<Match>().Reverse())
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
var command = match.Groups[1].Value.Trim();
|
var command = match.Groups[1].Value.Trim();
|
||||||
var output = await ExecuteInlineShellAsync(command, shellKind, workFolder, ct).ConfigureAwait(false);
|
var output = blockedReason ?? await ExecuteInlineShellAsync(command, shellKind, workFolder, ct).ConfigureAwait(false);
|
||||||
result = result.Remove(match.Index, match.Length).Insert(match.Index, output);
|
result = result.Remove(match.Index, match.Length).Insert(match.Index, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1425,6 +1557,19 @@ public static class SkillService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static string? GetInlineShellBlockedReason(SkillDefinition skill, Models.LlmSettings? llm)
|
||||||
|
{
|
||||||
|
if (llm is { EnableSkillInlineShell: false })
|
||||||
|
return "[inline-shell disabled by settings]";
|
||||||
|
|
||||||
|
return (skill.SourceScope ?? "").Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"plugin" when !(llm?.AllowPluginSkillInlineShell ?? false) => "[inline-shell blocked for plugin skills]",
|
||||||
|
"mcp" when !(llm?.AllowMcpSkillInlineShell ?? false) => "[inline-shell blocked for mcp skills]",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static string NormalizeEffort(string raw)
|
private static string NormalizeEffort(string raw)
|
||||||
{
|
{
|
||||||
var effort = raw.Trim().ToLowerInvariant();
|
var effort = raw.Trim().ToLowerInvariant();
|
||||||
@@ -1734,6 +1879,8 @@ public sealed record CompiledSkillInvocation(
|
|||||||
string SystemPrompt,
|
string SystemPrompt,
|
||||||
string DisplayText);
|
string DisplayText);
|
||||||
|
|
||||||
|
internal sealed record SkillLoadCandidate(int LoadOrder, SkillDefinition Skill);
|
||||||
|
|
||||||
internal sealed record SkillSourceDescriptor(string Directory, string Scope, SkillSourceKind Kind);
|
internal sealed record SkillSourceDescriptor(string Directory, string Scope, SkillSourceKind Kind);
|
||||||
|
|
||||||
internal enum SkillSourceKind
|
internal enum SkillSourceKind
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ public class ToolRegistry : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
McpSkillCatalog.RefreshFromClients(_mcpClients.Values);
|
||||||
return registered;
|
return registered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -546,6 +546,27 @@ public class SettingsViewModel : INotifyPropertyChanged
|
|||||||
set { _additionalSkillFoldersText = value; OnPropertyChanged(); }
|
set { _additionalSkillFoldersText = value; OnPropertyChanged(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _enableManagedSkillSource = true;
|
||||||
|
public bool EnableManagedSkillSource
|
||||||
|
{
|
||||||
|
get => _enableManagedSkillSource;
|
||||||
|
set { _enableManagedSkillSource = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _enableUserSkillSource = true;
|
||||||
|
public bool EnableUserSkillSource
|
||||||
|
{
|
||||||
|
get => _enableUserSkillSource;
|
||||||
|
set { _enableUserSkillSource = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _enableAdditionalSkillDiscovery = true;
|
||||||
|
public bool EnableAdditionalSkillDiscovery
|
||||||
|
{
|
||||||
|
get => _enableAdditionalSkillDiscovery;
|
||||||
|
set { _enableAdditionalSkillDiscovery = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
private bool _enableProjectSkillDiscovery = true;
|
private bool _enableProjectSkillDiscovery = true;
|
||||||
public bool EnableProjectSkillDiscovery
|
public bool EnableProjectSkillDiscovery
|
||||||
{
|
{
|
||||||
@@ -560,6 +581,20 @@ public class SettingsViewModel : INotifyPropertyChanged
|
|||||||
set { _enablePluginSkillDiscovery = value; OnPropertyChanged(); }
|
set { _enablePluginSkillDiscovery = value; OnPropertyChanged(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _enableMcpSkillDiscovery = true;
|
||||||
|
public bool EnableMcpSkillDiscovery
|
||||||
|
{
|
||||||
|
get => _enableMcpSkillDiscovery;
|
||||||
|
set { _enableMcpSkillDiscovery = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _enablePluginOnlySkillMode;
|
||||||
|
public bool EnablePluginOnlySkillMode
|
||||||
|
{
|
||||||
|
get => _enablePluginOnlySkillMode;
|
||||||
|
set { _enablePluginOnlySkillMode = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
private bool _enableLegacyCommandSkills = true;
|
private bool _enableLegacyCommandSkills = true;
|
||||||
public bool EnableLegacyCommandSkills
|
public bool EnableLegacyCommandSkills
|
||||||
{
|
{
|
||||||
@@ -574,6 +609,20 @@ public class SettingsViewModel : INotifyPropertyChanged
|
|||||||
set { _enableSkillInlineShell = value; OnPropertyChanged(); }
|
set { _enableSkillInlineShell = value; OnPropertyChanged(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _allowPluginSkillInlineShell;
|
||||||
|
public bool AllowPluginSkillInlineShell
|
||||||
|
{
|
||||||
|
get => _allowPluginSkillInlineShell;
|
||||||
|
set { _allowPluginSkillInlineShell = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _allowMcpSkillInlineShell;
|
||||||
|
public bool AllowMcpSkillInlineShell
|
||||||
|
{
|
||||||
|
get => _allowMcpSkillInlineShell;
|
||||||
|
set { _allowMcpSkillInlineShell = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
private int _skillInlineShellTimeoutSeconds = 8;
|
private int _skillInlineShellTimeoutSeconds = 8;
|
||||||
public int SkillInlineShellTimeoutSeconds
|
public int SkillInlineShellTimeoutSeconds
|
||||||
{
|
{
|
||||||
@@ -1259,10 +1308,17 @@ public class SettingsViewModel : INotifyPropertyChanged
|
|||||||
_enableForkSkillDelegationEnforcement = llm.EnableForkSkillDelegationEnforcement;
|
_enableForkSkillDelegationEnforcement = llm.EnableForkSkillDelegationEnforcement;
|
||||||
_skillsFolderPath = llm.SkillsFolderPath;
|
_skillsFolderPath = llm.SkillsFolderPath;
|
||||||
_additionalSkillFoldersText = string.Join(Environment.NewLine, llm.AdditionalSkillFolders ?? new List<string>());
|
_additionalSkillFoldersText = string.Join(Environment.NewLine, llm.AdditionalSkillFolders ?? new List<string>());
|
||||||
|
_enableManagedSkillSource = llm.EnableManagedSkillSource;
|
||||||
|
_enableUserSkillSource = llm.EnableUserSkillSource;
|
||||||
|
_enableAdditionalSkillDiscovery = llm.EnableAdditionalSkillDiscovery;
|
||||||
_enableProjectSkillDiscovery = llm.EnableProjectSkillDiscovery;
|
_enableProjectSkillDiscovery = llm.EnableProjectSkillDiscovery;
|
||||||
_enablePluginSkillDiscovery = llm.EnablePluginSkillDiscovery;
|
_enablePluginSkillDiscovery = llm.EnablePluginSkillDiscovery;
|
||||||
|
_enableMcpSkillDiscovery = llm.EnableMcpSkillDiscovery;
|
||||||
|
_enablePluginOnlySkillMode = llm.EnablePluginOnlySkillMode;
|
||||||
_enableLegacyCommandSkills = llm.EnableLegacyCommandSkills;
|
_enableLegacyCommandSkills = llm.EnableLegacyCommandSkills;
|
||||||
_enableSkillInlineShell = llm.EnableSkillInlineShell;
|
_enableSkillInlineShell = llm.EnableSkillInlineShell;
|
||||||
|
_allowPluginSkillInlineShell = llm.AllowPluginSkillInlineShell;
|
||||||
|
_allowMcpSkillInlineShell = llm.AllowMcpSkillInlineShell;
|
||||||
_skillInlineShellTimeoutSeconds = Math.Clamp(llm.SkillInlineShellTimeoutSeconds, 1, 30);
|
_skillInlineShellTimeoutSeconds = Math.Clamp(llm.SkillInlineShellTimeoutSeconds, 1, 30);
|
||||||
_skillInlineShellMaxOutputChars = Math.Clamp(llm.SkillInlineShellMaxOutputChars, 200, 20000);
|
_skillInlineShellMaxOutputChars = Math.Clamp(llm.SkillInlineShellMaxOutputChars, 200, 20000);
|
||||||
_slashPopupPageSize = llm.SlashPopupPageSize > 0 ? Math.Clamp(llm.SlashPopupPageSize, 3, 10) : 6;
|
_slashPopupPageSize = llm.SlashPopupPageSize > 0 ? Math.Clamp(llm.SlashPopupPageSize, 3, 10) : 6;
|
||||||
@@ -1716,10 +1772,17 @@ public class SettingsViewModel : INotifyPropertyChanged
|
|||||||
s.Llm.EnableForkSkillDelegationEnforcement = _enableForkSkillDelegationEnforcement;
|
s.Llm.EnableForkSkillDelegationEnforcement = _enableForkSkillDelegationEnforcement;
|
||||||
s.Llm.SkillsFolderPath = _skillsFolderPath;
|
s.Llm.SkillsFolderPath = _skillsFolderPath;
|
||||||
s.Llm.AdditionalSkillFolders = ParseAdditionalSkillFolders(_additionalSkillFoldersText);
|
s.Llm.AdditionalSkillFolders = ParseAdditionalSkillFolders(_additionalSkillFoldersText);
|
||||||
|
s.Llm.EnableManagedSkillSource = _enableManagedSkillSource;
|
||||||
|
s.Llm.EnableUserSkillSource = _enableUserSkillSource;
|
||||||
|
s.Llm.EnableAdditionalSkillDiscovery = _enableAdditionalSkillDiscovery;
|
||||||
s.Llm.EnableProjectSkillDiscovery = _enableProjectSkillDiscovery;
|
s.Llm.EnableProjectSkillDiscovery = _enableProjectSkillDiscovery;
|
||||||
s.Llm.EnablePluginSkillDiscovery = _enablePluginSkillDiscovery;
|
s.Llm.EnablePluginSkillDiscovery = _enablePluginSkillDiscovery;
|
||||||
|
s.Llm.EnableMcpSkillDiscovery = _enableMcpSkillDiscovery;
|
||||||
|
s.Llm.EnablePluginOnlySkillMode = _enablePluginOnlySkillMode;
|
||||||
s.Llm.EnableLegacyCommandSkills = _enableLegacyCommandSkills;
|
s.Llm.EnableLegacyCommandSkills = _enableLegacyCommandSkills;
|
||||||
s.Llm.EnableSkillInlineShell = _enableSkillInlineShell;
|
s.Llm.EnableSkillInlineShell = _enableSkillInlineShell;
|
||||||
|
s.Llm.AllowPluginSkillInlineShell = _allowPluginSkillInlineShell;
|
||||||
|
s.Llm.AllowMcpSkillInlineShell = _allowMcpSkillInlineShell;
|
||||||
s.Llm.SkillInlineShellTimeoutSeconds = _skillInlineShellTimeoutSeconds;
|
s.Llm.SkillInlineShellTimeoutSeconds = _skillInlineShellTimeoutSeconds;
|
||||||
s.Llm.SkillInlineShellMaxOutputChars = _skillInlineShellMaxOutputChars;
|
s.Llm.SkillInlineShellMaxOutputChars = _skillInlineShellMaxOutputChars;
|
||||||
s.Llm.SlashPopupPageSize = _slashPopupPageSize;
|
s.Llm.SlashPopupPageSize = _slashPopupPageSize;
|
||||||
|
|||||||
@@ -869,6 +869,42 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Style="{StaticResource ToggleSwitch}"/>
|
Style="{StaticResource ToggleSwitch}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid Margin="0,10,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="기본 제공 스킬 소스"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource PrimaryText}"/>
|
||||||
|
<CheckBox x:Name="ChkEnableManagedSkillSource"
|
||||||
|
Grid.Column="1"
|
||||||
|
Style="{StaticResource ToggleSwitch}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,10,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="사용자 스킬 소스"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource PrimaryText}"/>
|
||||||
|
<CheckBox x:Name="ChkEnableUserSkillSource"
|
||||||
|
Grid.Column="1"
|
||||||
|
Style="{StaticResource ToggleSwitch}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,10,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="보조 스킬 폴더 탐색"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource PrimaryText}"/>
|
||||||
|
<CheckBox x:Name="ChkEnableAdditionalSkillDiscovery"
|
||||||
|
Grid.Column="1"
|
||||||
|
Style="{StaticResource ToggleSwitch}"/>
|
||||||
|
</Grid>
|
||||||
<Grid Margin="0,10,0,0">
|
<Grid Margin="0,10,0,0">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
@@ -893,6 +929,30 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Style="{StaticResource ToggleSwitch}"/>
|
Style="{StaticResource ToggleSwitch}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid Margin="0,10,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="MCP 스킬 탐색"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource PrimaryText}"/>
|
||||||
|
<CheckBox x:Name="ChkEnableMcpSkillDiscovery"
|
||||||
|
Grid.Column="1"
|
||||||
|
Style="{StaticResource ToggleSwitch}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,10,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="플러그인 전용 모드"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource PrimaryText}"/>
|
||||||
|
<CheckBox x:Name="ChkEnablePluginOnlySkillMode"
|
||||||
|
Grid.Column="1"
|
||||||
|
Style="{StaticResource ToggleSwitch}"/>
|
||||||
|
</Grid>
|
||||||
<Grid Margin="0,10,0,0">
|
<Grid Margin="0,10,0,0">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
@@ -917,6 +977,30 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Style="{StaticResource ToggleSwitch}"/>
|
Style="{StaticResource ToggleSwitch}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid Margin="0,10,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="플러그인 inline shell 허용"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource PrimaryText}"/>
|
||||||
|
<CheckBox x:Name="ChkAllowPluginSkillInlineShell"
|
||||||
|
Grid.Column="1"
|
||||||
|
Style="{StaticResource ToggleSwitch}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,10,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="MCP inline shell 허용"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource PrimaryText}"/>
|
||||||
|
<CheckBox x:Name="ChkAllowMcpSkillInlineShell"
|
||||||
|
Grid.Column="1"
|
||||||
|
Style="{StaticResource ToggleSwitch}"/>
|
||||||
|
</Grid>
|
||||||
<StackPanel Margin="0,12,0,0">
|
<StackPanel Margin="0,12,0,0">
|
||||||
<TextBlock Text="보조 스킬 폴더 목록"
|
<TextBlock Text="보조 스킬 폴더 목록"
|
||||||
Foreground="{DynamicResource PrimaryText}"/>
|
Foreground="{DynamicResource PrimaryText}"/>
|
||||||
@@ -1053,7 +1137,7 @@
|
|||||||
FontSize="14"
|
FontSize="14"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="{DynamicResource PrimaryText}"/>
|
Foreground="{DynamicResource PrimaryText}"/>
|
||||||
<TextBlock Text="현재 AX Agent에서 사용할 수 있는 슬래시 스킬 목록입니다."
|
<TextBlock Text="현재 AX Agent에서 사용할 수 있는 슬래시 스킬 목록입니다. 프로젝트 `.claude/skills`와 MCP 스킬 소스도 함께 반영됩니다."
|
||||||
Margin="0,4,0,8"
|
Margin="0,4,0,8"
|
||||||
FontSize="11.5"
|
FontSize="11.5"
|
||||||
Foreground="{DynamicResource SecondaryText}"/>
|
Foreground="{DynamicResource SecondaryText}"/>
|
||||||
|
|||||||
@@ -66,10 +66,17 @@ public partial class AgentSettingsWindow : Window
|
|||||||
ChkEnableToolHooks.IsChecked = _llm.EnableToolHooks;
|
ChkEnableToolHooks.IsChecked = _llm.EnableToolHooks;
|
||||||
ChkEnableHookInputMutation.IsChecked = _llm.EnableHookInputMutation;
|
ChkEnableHookInputMutation.IsChecked = _llm.EnableHookInputMutation;
|
||||||
ChkEnableHookPermissionUpdate.IsChecked = _llm.EnableHookPermissionUpdate;
|
ChkEnableHookPermissionUpdate.IsChecked = _llm.EnableHookPermissionUpdate;
|
||||||
|
ChkEnableManagedSkillSource.IsChecked = _llm.EnableManagedSkillSource;
|
||||||
|
ChkEnableUserSkillSource.IsChecked = _llm.EnableUserSkillSource;
|
||||||
|
ChkEnableAdditionalSkillDiscovery.IsChecked = _llm.EnableAdditionalSkillDiscovery;
|
||||||
ChkEnableProjectSkillDiscovery.IsChecked = _llm.EnableProjectSkillDiscovery;
|
ChkEnableProjectSkillDiscovery.IsChecked = _llm.EnableProjectSkillDiscovery;
|
||||||
ChkEnablePluginSkillDiscovery.IsChecked = _llm.EnablePluginSkillDiscovery;
|
ChkEnablePluginSkillDiscovery.IsChecked = _llm.EnablePluginSkillDiscovery;
|
||||||
|
ChkEnableMcpSkillDiscovery.IsChecked = _llm.EnableMcpSkillDiscovery;
|
||||||
|
ChkEnablePluginOnlySkillMode.IsChecked = _llm.EnablePluginOnlySkillMode;
|
||||||
ChkEnableLegacyCommandSkills.IsChecked = _llm.EnableLegacyCommandSkills;
|
ChkEnableLegacyCommandSkills.IsChecked = _llm.EnableLegacyCommandSkills;
|
||||||
ChkEnableSkillInlineShell.IsChecked = _llm.EnableSkillInlineShell;
|
ChkEnableSkillInlineShell.IsChecked = _llm.EnableSkillInlineShell;
|
||||||
|
ChkAllowPluginSkillInlineShell.IsChecked = _llm.AllowPluginSkillInlineShell;
|
||||||
|
ChkAllowMcpSkillInlineShell.IsChecked = _llm.AllowMcpSkillInlineShell;
|
||||||
ChkEnableCoworkVerification.IsChecked = _llm.EnableCoworkVerification;
|
ChkEnableCoworkVerification.IsChecked = _llm.EnableCoworkVerification;
|
||||||
ChkEnableProjectRules.IsChecked = _llm.EnableProjectRules;
|
ChkEnableProjectRules.IsChecked = _llm.EnableProjectRules;
|
||||||
ChkEnableAgentMemory.IsChecked = _llm.EnableAgentMemory;
|
ChkEnableAgentMemory.IsChecked = _llm.EnableAgentMemory;
|
||||||
@@ -550,10 +557,17 @@ public partial class AgentSettingsWindow : Window
|
|||||||
_llm.EnableToolHooks = ChkEnableToolHooks.IsChecked == true;
|
_llm.EnableToolHooks = ChkEnableToolHooks.IsChecked == true;
|
||||||
_llm.EnableHookInputMutation = ChkEnableHookInputMutation.IsChecked == true;
|
_llm.EnableHookInputMutation = ChkEnableHookInputMutation.IsChecked == true;
|
||||||
_llm.EnableHookPermissionUpdate = ChkEnableHookPermissionUpdate.IsChecked == true;
|
_llm.EnableHookPermissionUpdate = ChkEnableHookPermissionUpdate.IsChecked == true;
|
||||||
|
_llm.EnableManagedSkillSource = ChkEnableManagedSkillSource.IsChecked == true;
|
||||||
|
_llm.EnableUserSkillSource = ChkEnableUserSkillSource.IsChecked == true;
|
||||||
|
_llm.EnableAdditionalSkillDiscovery = ChkEnableAdditionalSkillDiscovery.IsChecked == true;
|
||||||
_llm.EnableProjectSkillDiscovery = ChkEnableProjectSkillDiscovery.IsChecked == true;
|
_llm.EnableProjectSkillDiscovery = ChkEnableProjectSkillDiscovery.IsChecked == true;
|
||||||
_llm.EnablePluginSkillDiscovery = ChkEnablePluginSkillDiscovery.IsChecked == true;
|
_llm.EnablePluginSkillDiscovery = ChkEnablePluginSkillDiscovery.IsChecked == true;
|
||||||
|
_llm.EnableMcpSkillDiscovery = ChkEnableMcpSkillDiscovery.IsChecked == true;
|
||||||
|
_llm.EnablePluginOnlySkillMode = ChkEnablePluginOnlySkillMode.IsChecked == true;
|
||||||
_llm.EnableLegacyCommandSkills = ChkEnableLegacyCommandSkills.IsChecked == true;
|
_llm.EnableLegacyCommandSkills = ChkEnableLegacyCommandSkills.IsChecked == true;
|
||||||
_llm.EnableSkillInlineShell = ChkEnableSkillInlineShell.IsChecked == true;
|
_llm.EnableSkillInlineShell = ChkEnableSkillInlineShell.IsChecked == true;
|
||||||
|
_llm.AllowPluginSkillInlineShell = ChkAllowPluginSkillInlineShell.IsChecked == true;
|
||||||
|
_llm.AllowMcpSkillInlineShell = ChkAllowMcpSkillInlineShell.IsChecked == true;
|
||||||
_llm.EnableCoworkVerification = ChkEnableCoworkVerification.IsChecked == true;
|
_llm.EnableCoworkVerification = ChkEnableCoworkVerification.IsChecked == true;
|
||||||
_llm.EnableProjectRules = ChkEnableProjectRules.IsChecked == true;
|
_llm.EnableProjectRules = ChkEnableProjectRules.IsChecked == true;
|
||||||
_llm.EnableAgentMemory = ChkEnableAgentMemory.IsChecked == true;
|
_llm.EnableAgentMemory = ChkEnableAgentMemory.IsChecked == true;
|
||||||
@@ -656,7 +670,7 @@ public partial class AgentSettingsWindow : Window
|
|||||||
Padding = new Thickness(12, 10, 12, 10),
|
Padding = new Thickness(12, 10, 12, 10),
|
||||||
Child = new TextBlock
|
Child = new TextBlock
|
||||||
{
|
{
|
||||||
Text = "로드된 스킬이 없습니다. 기본/추가 스킬 폴더 또는 프로젝트 `.claude/skills` 아래에 `.skill.md`나 `SKILL.md`를 추가한 뒤 저장하면 다시 불러옵니다.",
|
Text = "로드된 스킬이 없습니다. 기본/추가 스킬 폴더, 프로젝트 `.claude/skills`, 연결된 MCP 스킬 소스를 확인한 뒤 다시 불러오세요.",
|
||||||
FontSize = 11,
|
FontSize = 11,
|
||||||
TextWrapping = TextWrapping.Wrap,
|
TextWrapping = TextWrapping.Wrap,
|
||||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||||
@@ -671,10 +685,11 @@ public partial class AgentSettingsWindow : Window
|
|||||||
new { Title = "기본 제공 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "managed", StringComparison.OrdinalIgnoreCase)).ToList() },
|
new { Title = "기본 제공 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "managed", StringComparison.OrdinalIgnoreCase)).ToList() },
|
||||||
new { Title = "프로젝트 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase)).ToList() },
|
new { Title = "프로젝트 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase)).ToList() },
|
||||||
new { Title = "플러그인 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase)).ToList() },
|
new { Title = "플러그인 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase)).ToList() },
|
||||||
|
new { Title = "MCP 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "mcp", StringComparison.OrdinalIgnoreCase)).ToList() },
|
||||||
new { Title = "보조/공용 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "additional", StringComparison.OrdinalIgnoreCase)).ToList() },
|
new { Title = "보조/공용 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "additional", StringComparison.OrdinalIgnoreCase)).ToList() },
|
||||||
new { Title = "레거시 명령 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "legacy", StringComparison.OrdinalIgnoreCase)).ToList() },
|
new { Title = "레거시 명령 스킬", Items = skills.Where(s => string.Equals(s.SourceScope, "legacy", StringComparison.OrdinalIgnoreCase)).ToList() },
|
||||||
new { Title = "사용자 스킬", Items = skills.Where(s => !string.Equals(s.SourceScope, "bundled", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "managed", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "additional", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "legacy", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(s.Requires)).ToList() },
|
new { Title = "사용자 스킬", Items = skills.Where(s => !string.Equals(s.SourceScope, "bundled", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "managed", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "mcp", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "additional", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "legacy", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(s.Requires)).ToList() },
|
||||||
new { Title = "고급 스킬", Items = skills.Where(s => !string.IsNullOrWhiteSpace(s.Requires) && !string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase)).ToList() },
|
new { Title = "고급 스킬", Items = skills.Where(s => !string.IsNullOrWhiteSpace(s.Requires) && !string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase) && !string.Equals(s.SourceScope, "mcp", StringComparison.OrdinalIgnoreCase)).ToList() },
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var group in groups)
|
foreach (var group in groups)
|
||||||
|
|||||||
@@ -3529,8 +3529,13 @@ public partial class ChatWindow : Window
|
|||||||
// 탭별 필터링: Chat → "all"만, Cowork/Code → "all" + "dev"
|
// 탭별 필터링: Chat → "all"만, Cowork/Code → "all" + "dev"
|
||||||
bool isDev = _activeTab is "Cowork" or "Code";
|
bool isDev = _activeTab is "Cowork" or "Code";
|
||||||
|
|
||||||
// 내장 슬래시 명령어 매칭 (탭 필터)
|
var slashEntries = SlashCommandCatalog.MatchBuiltinCommands(text, isDev)
|
||||||
var matches = SlashCommandCatalog.MatchBuiltinCommands(text, isDev);
|
.Select(match => (
|
||||||
|
match.Cmd,
|
||||||
|
match.Label,
|
||||||
|
match.IsSkill,
|
||||||
|
Priority: SlashCommandCatalog.GetBuiltInCommandPriority(match.Cmd)))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// 스킬 슬래시 명령어 매칭 (탭별 필터)
|
// 스킬 슬래시 명령어 매칭 (탭별 필터)
|
||||||
if (_settings.Settings.Llm.EnableSkillSystem)
|
if (_settings.Settings.Llm.EnableSkillSystem)
|
||||||
@@ -3540,11 +3545,12 @@ public partial class ChatWindow : Window
|
|||||||
.Where(s => s.IsVisibleInTab(_activeTab))
|
.Where(s => s.IsVisibleInTab(_activeTab))
|
||||||
.Select(s => (Cmd: "/" + s.Name,
|
.Select(s => (Cmd: "/" + s.Name,
|
||||||
Label: BuildSlashSkillLabel(s),
|
Label: BuildSlashSkillLabel(s),
|
||||||
IsSkill: true, Available: s.IsAvailable));
|
IsSkill: true,
|
||||||
foreach (var sm in skillMatches)
|
Priority: SkillService.GetSkillSourcePriority(s.SourceScope)));
|
||||||
matches.Add((sm.Cmd, sm.Label, sm.IsSkill));
|
slashEntries.AddRange(skillMatches);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var matches = SlashCommandCatalog.ComposeMatches(slashEntries);
|
||||||
if (matches.Count > 0)
|
if (matches.Count > 0)
|
||||||
{
|
{
|
||||||
_slashPalette.Matches = matches;
|
_slashPalette.Matches = matches;
|
||||||
|
|||||||
@@ -4364,6 +4364,39 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource SettingsRow}">
|
||||||
|
<Grid>
|
||||||
|
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||||
|
<TextBlock Style="{StaticResource RowLabel}" Text="기본 제공 스킬 소스"/>
|
||||||
|
<TextBlock Style="{StaticResource RowHint}" Text="앱에 함께 배포되는 관리형 스킬 파일 소스를 로드합니다. 번들 스킬은 항상 유지됩니다."/>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding EnableManagedSkillSource, Mode=TwoWay}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource SettingsRow}">
|
||||||
|
<Grid>
|
||||||
|
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||||
|
<TextBlock Style="{StaticResource RowLabel}" Text="사용자 스킬 소스"/>
|
||||||
|
<TextBlock Style="{StaticResource RowHint}" Text="%APPDATA%\AxCopilot\skills\와 직접 지정한 기본 스킬 폴더를 함께 로드합니다."/>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding EnableUserSkillSource, Mode=TwoWay}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource SettingsRow}">
|
||||||
|
<Grid>
|
||||||
|
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||||
|
<TextBlock Style="{StaticResource RowLabel}" Text="보조 스킬 폴더 탐색"/>
|
||||||
|
<TextBlock Style="{StaticResource RowHint}" Text="보조 스킬 폴더 목록에 입력한 공용/팀 스킬 폴더를 실제 로딩 대상에 포함합니다."/>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding EnableAdditionalSkillDiscovery, Mode=TwoWay}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Border Style="{StaticResource SettingsRow}">
|
<Border Style="{StaticResource SettingsRow}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||||
@@ -4386,6 +4419,28 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource SettingsRow}">
|
||||||
|
<Grid>
|
||||||
|
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||||
|
<TextBlock Style="{StaticResource RowLabel}" Text="MCP 스킬 탐색"/>
|
||||||
|
<TextBlock Style="{StaticResource RowHint}" Text="연결된 MCP 서버의 리소스/도구 메타데이터를 보조 스킬처럼 노출합니다."/>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding EnableMcpSkillDiscovery, Mode=TwoWay}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource SettingsRow}">
|
||||||
|
<Grid>
|
||||||
|
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||||
|
<TextBlock Style="{StaticResource RowLabel}" Text="플러그인 전용 스킬 모드"/>
|
||||||
|
<TextBlock Style="{StaticResource RowHint}" Text="활성화하면 기본 제공/플러그인 스킬만 노출하고 다른 소스는 숨깁니다."/>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding EnablePluginOnlySkillMode, Mode=TwoWay}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Border Style="{StaticResource SettingsRow}">
|
<Border Style="{StaticResource SettingsRow}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||||
@@ -4408,6 +4463,28 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource SettingsRow}">
|
||||||
|
<Grid>
|
||||||
|
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||||
|
<TextBlock Style="{StaticResource RowLabel}" Text="플러그인 inline shell 허용"/>
|
||||||
|
<TextBlock Style="{StaticResource RowHint}" Text="플러그인 소스 스킬에 한해 inline shell 실행을 별도로 허용합니다."/>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding AllowPluginSkillInlineShell, Mode=TwoWay}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource SettingsRow}">
|
||||||
|
<Grid>
|
||||||
|
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||||
|
<TextBlock Style="{StaticResource RowLabel}" Text="MCP inline shell 허용"/>
|
||||||
|
<TextBlock Style="{StaticResource RowHint}" Text="MCP 소스 스킬에는 기본적으로 inline shell을 막고, 필요 시에만 별도 허용합니다."/>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding AllowMcpSkillInlineShell, Mode=TwoWay}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Border Style="{StaticResource SettingsRow}">
|
<Border Style="{StaticResource SettingsRow}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<StackPanel HorizontalAlignment="Left">
|
<StackPanel HorizontalAlignment="Left">
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ public partial class SkillGalleryWindow : Window
|
|||||||
"기본 제공" => skills.Where(IsBuiltInSkill).ToList(),
|
"기본 제공" => skills.Where(IsBuiltInSkill).ToList(),
|
||||||
"프로젝트" => skills.Where(s => string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase)).ToList(),
|
"프로젝트" => skills.Where(s => string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase)).ToList(),
|
||||||
"플러그인" => skills.Where(s => string.Equals(s.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase)).ToList(),
|
"플러그인" => skills.Where(s => string.Equals(s.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase)).ToList(),
|
||||||
|
"MCP" => skills.Where(s => string.Equals(s.SourceScope, "mcp", StringComparison.OrdinalIgnoreCase)).ToList(),
|
||||||
"고급 (런타임)" => skills.Where(s => !string.IsNullOrEmpty(s.Requires)).ToList(),
|
"고급 (런타임)" => skills.Where(s => !string.IsNullOrEmpty(s.Requires)).ToList(),
|
||||||
"사용자" => skills.Where(IsUserOwnedSkill).ToList(),
|
"사용자" => skills.Where(IsUserOwnedSkill).ToList(),
|
||||||
_ => skills.ToList(),
|
_ => skills.ToList(),
|
||||||
@@ -164,6 +165,8 @@ public partial class SkillGalleryWindow : Window
|
|||||||
var isBuiltIn = IsBuiltInSkill(skill);
|
var isBuiltIn = IsBuiltInSkill(skill);
|
||||||
var isProject = string.Equals(skill.SourceScope, "project", StringComparison.OrdinalIgnoreCase);
|
var isProject = string.Equals(skill.SourceScope, "project", StringComparison.OrdinalIgnoreCase);
|
||||||
var isPlugin = string.Equals(skill.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase);
|
var isPlugin = string.Equals(skill.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isMcp = string.Equals(skill.SourceScope, "mcp", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var hasBackingFile = !string.IsNullOrWhiteSpace(skill.FilePath) && File.Exists(skill.FilePath);
|
||||||
|
|
||||||
var card = new Border
|
var card = new Border
|
||||||
{
|
{
|
||||||
@@ -231,6 +234,8 @@ public partial class SkillGalleryWindow : Window
|
|||||||
nameRow.Children.Add(MakeBadge("프로젝트", "#2563EB"));
|
nameRow.Children.Add(MakeBadge("프로젝트", "#2563EB"));
|
||||||
else if (isPlugin)
|
else if (isPlugin)
|
||||||
nameRow.Children.Add(MakeBadge("플러그인", "#EC4899"));
|
nameRow.Children.Add(MakeBadge("플러그인", "#EC4899"));
|
||||||
|
else if (isMcp)
|
||||||
|
nameRow.Children.Add(MakeBadge("MCP", "#14B8A6"));
|
||||||
else if (isUser)
|
else if (isUser)
|
||||||
nameRow.Children.Add(MakeBadge("사용자", "#34D399"));
|
nameRow.Children.Add(MakeBadge("사용자", "#34D399"));
|
||||||
else if (isAdvanced)
|
else if (isAdvanced)
|
||||||
@@ -266,62 +271,68 @@ public partial class SkillGalleryWindow : Window
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 편집
|
// 편집
|
||||||
actions.Children.Add(MakeActionBtn("\uE70F", "#3B82F6", isUser ? "편집 (시각적 편집기)" : "편집 (파일 열기)",
|
if (isUser || hasBackingFile)
|
||||||
() =>
|
{
|
||||||
{
|
actions.Children.Add(MakeActionBtn("\uE70F", "#3B82F6", isUser ? "편집 (시각적 편집기)" : "편집 (파일 열기)",
|
||||||
if (isUser)
|
() =>
|
||||||
{
|
{
|
||||||
var editor = new SkillEditorWindow(skill) { Owner = this };
|
if (isUser)
|
||||||
if (editor.ShowDialog() == true)
|
|
||||||
{
|
{
|
||||||
|
var editor = new SkillEditorWindow(skill) { Owner = this };
|
||||||
|
if (editor.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
SkillService.ReloadFromCurrentSettings();
|
||||||
|
BuildCategoryFilter();
|
||||||
|
RenderSkills();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(skill.FilePath) { UseShellExecute = true }); }
|
||||||
|
catch (Exception ex) { CustomMessageBox.Show($"파일을 열 수 없습니다: {ex.Message}", "편집"); }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 복제 (파일 기반 스킬만)
|
||||||
|
if (hasBackingFile)
|
||||||
|
{
|
||||||
|
actions.Children.Add(MakeActionBtn("\uE8C8", "#10B981", "복제",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var destFolder = Path.Combine(userFolder);
|
||||||
|
if (!Directory.Exists(destFolder)) Directory.CreateDirectory(destFolder);
|
||||||
|
|
||||||
|
var srcName = Path.GetFileNameWithoutExtension(skill.FilePath);
|
||||||
|
var destPath = Path.Combine(destFolder, $"{srcName}_copy.skill.md");
|
||||||
|
var counter = 2;
|
||||||
|
while (File.Exists(destPath))
|
||||||
|
destPath = Path.Combine(destFolder, $"{srcName}_copy{counter++}.skill.md");
|
||||||
|
|
||||||
|
File.Copy(skill.FilePath, destPath);
|
||||||
SkillService.ReloadFromCurrentSettings();
|
SkillService.ReloadFromCurrentSettings();
|
||||||
BuildCategoryFilter();
|
BuildCategoryFilter();
|
||||||
RenderSkills();
|
RenderSkills();
|
||||||
}
|
}
|
||||||
}
|
catch (Exception ex) { CustomMessageBox.Show($"복제 실패: {ex.Message}", "복제"); }
|
||||||
else
|
}));
|
||||||
|
|
||||||
|
// 내보내기
|
||||||
|
actions.Children.Add(MakeActionBtn("\uEDE1", "#F59E0B", "내보내기 (.zip)",
|
||||||
|
() =>
|
||||||
{
|
{
|
||||||
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(skill.FilePath) { UseShellExecute = true }); }
|
var folderDlg = new System.Windows.Forms.FolderBrowserDialog
|
||||||
catch (Exception ex) { CustomMessageBox.Show($"파일을 열 수 없습니다: {ex.Message}", "편집"); }
|
{ Description = "내보낼 폴더를 선택하세요" };
|
||||||
}
|
if (folderDlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
||||||
}));
|
var result = SkillService.ExportSkill(skill, folderDlg.SelectedPath);
|
||||||
|
if (result != null)
|
||||||
// 복제 (사용자 스킬/폴더 스킬만)
|
CustomMessageBox.Show($"내보내기 완료:\n{result}", "내보내기");
|
||||||
actions.Children.Add(MakeActionBtn("\uE8C8", "#10B981", "복제",
|
else
|
||||||
() =>
|
CustomMessageBox.Show("내보내기에 실패했습니다.", "내보내기");
|
||||||
{
|
}));
|
||||||
try
|
}
|
||||||
{
|
|
||||||
var destFolder = Path.Combine(userFolder);
|
|
||||||
if (!Directory.Exists(destFolder)) Directory.CreateDirectory(destFolder);
|
|
||||||
|
|
||||||
var srcName = Path.GetFileNameWithoutExtension(skill.FilePath);
|
|
||||||
var destPath = Path.Combine(destFolder, $"{srcName}_copy.skill.md");
|
|
||||||
var counter = 2;
|
|
||||||
while (File.Exists(destPath))
|
|
||||||
destPath = Path.Combine(destFolder, $"{srcName}_copy{counter++}.skill.md");
|
|
||||||
|
|
||||||
File.Copy(skill.FilePath, destPath);
|
|
||||||
SkillService.ReloadFromCurrentSettings();
|
|
||||||
BuildCategoryFilter();
|
|
||||||
RenderSkills();
|
|
||||||
}
|
|
||||||
catch (Exception ex) { CustomMessageBox.Show($"복제 실패: {ex.Message}", "복제"); }
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 내보내기
|
|
||||||
actions.Children.Add(MakeActionBtn("\uEDE1", "#F59E0B", "내보내기 (.zip)",
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
var folderDlg = new System.Windows.Forms.FolderBrowserDialog
|
|
||||||
{ Description = "내보낼 폴더를 선택하세요" };
|
|
||||||
if (folderDlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
|
||||||
var result = SkillService.ExportSkill(skill, folderDlg.SelectedPath);
|
|
||||||
if (result != null)
|
|
||||||
CustomMessageBox.Show($"내보내기 완료:\n{result}", "내보내기");
|
|
||||||
else
|
|
||||||
CustomMessageBox.Show("내보내기에 실패했습니다.", "내보내기");
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 삭제 (사용자 스킬만)
|
// 삭제 (사용자 스킬만)
|
||||||
if (isUser)
|
if (isUser)
|
||||||
@@ -405,6 +416,8 @@ public partial class SkillGalleryWindow : Window
|
|||||||
yield return "프로젝트";
|
yield return "프로젝트";
|
||||||
if (skills.Any(s => string.Equals(s.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase)))
|
if (skills.Any(s => string.Equals(s.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase)))
|
||||||
yield return "플러그인";
|
yield return "플러그인";
|
||||||
|
if (skills.Any(s => string.Equals(s.SourceScope, "mcp", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
yield return "MCP";
|
||||||
if (skills.Any(s => !string.IsNullOrEmpty(s.Requires)))
|
if (skills.Any(s => !string.IsNullOrEmpty(s.Requires)))
|
||||||
yield return "고급 (런타임)";
|
yield return "고급 (런타임)";
|
||||||
if (skills.Any(IsUserOwnedSkill))
|
if (skills.Any(IsUserOwnedSkill))
|
||||||
@@ -422,7 +435,8 @@ public partial class SkillGalleryWindow : Window
|
|||||||
string.Equals(skill.SourceScope, "legacy", StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(skill.SourceScope, "legacy", StringComparison.OrdinalIgnoreCase) ||
|
||||||
(!IsBuiltInSkill(skill)
|
(!IsBuiltInSkill(skill)
|
||||||
&& !string.Equals(skill.SourceScope, "project", StringComparison.OrdinalIgnoreCase)
|
&& !string.Equals(skill.SourceScope, "project", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(skill.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase));
|
&& !string.Equals(skill.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.Equals(skill.SourceScope, "mcp", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
private Border MakeActionBtn(string icon, string colorHex, string tooltip, Action action)
|
private Border MakeActionBtn(string icon, string colorHex, string tooltip, Action action)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
namespace AxCopilot.Views;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
internal static class SlashCommandCatalog
|
internal static class SlashCommandCatalog
|
||||||
{
|
{
|
||||||
@@ -108,6 +112,37 @@ internal static class SlashCommandCatalog
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static int GetBuiltInCommandPriority(string commandToken)
|
||||||
|
{
|
||||||
|
if (!Commands.TryGetValue(commandToken, out var entry))
|
||||||
|
return 2000;
|
||||||
|
|
||||||
|
return string.Equals(entry.Tab, "dev", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? 2100
|
||||||
|
: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static List<(string Cmd, string Label, bool IsSkill)> ComposeMatches(
|
||||||
|
IEnumerable<(string Cmd, string Label, bool IsSkill, int Priority)> entries)
|
||||||
|
{
|
||||||
|
if (entries == null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.Where(entry => !string.IsNullOrWhiteSpace(entry.Cmd))
|
||||||
|
.GroupBy(entry => entry.Cmd, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => group
|
||||||
|
.OrderByDescending(entry => entry.Priority)
|
||||||
|
.ThenBy(entry => entry.IsSkill ? 1 : 0)
|
||||||
|
.ThenBy(entry => entry.Cmd, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.First())
|
||||||
|
.Select(entry => (entry.Cmd, entry.Label, entry.IsSkill, entry.Priority))
|
||||||
|
.OrderByDescending(entry => entry.Priority)
|
||||||
|
.ThenBy(entry => entry.Cmd, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(entry => (entry.Cmd, entry.Label, entry.IsSkill))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
internal static bool TryGetEntry(string commandToken, out (string Label, string SystemPrompt, string Tab) entry)
|
internal static bool TryGetEntry(string commandToken, out (string Label, string SystemPrompt, string Tab) entry)
|
||||||
=> Commands.TryGetValue(commandToken, out entry);
|
=> Commands.TryGetValue(commandToken, out entry);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user