coding-tool-x 3.8.4 → 3.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +86 -15
- package/dist/web/assets/AgentsDrawer-BG6P3DX-.js +1 -0
- package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +1 -0
- package/dist/web/assets/Analytics--z_FmL5P.js +1 -0
- package/dist/web/assets/Analytics-CyHhR126.css +1 -0
- package/dist/web/assets/AssetPathField-CrJtRKHy.js +1 -0
- package/dist/web/assets/ChatHistoryDrawer-B2qBQs9L.css +1 -0
- package/dist/web/assets/ChatHistoryDrawer-BiuEryJj.js +1 -0
- package/dist/web/assets/CollapseItem-BAYEsL_Q.js +1 -0
- package/dist/web/assets/CommandsDrawer-BZJxlPvV.css +1 -0
- package/dist/web/assets/CommandsDrawer-YJ-U7BXN.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-Ce_hAknW.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-dwruRKWr.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bvr1NN6K.js +1 -0
- package/dist/web/assets/ConfigTemplatesDrawer-BfDi8yO3.css +1 -0
- package/dist/web/assets/ConfigTemplatesDrawer-BjIeHfF-.js +1 -0
- package/dist/web/assets/ConfigTemplatesPanel-AV00tm_D.js +1 -0
- package/dist/web/assets/ConfigTemplatesPanel-CWyPtk67.css +1 -0
- package/dist/web/assets/Divider-D28g1gwj.js +1 -0
- package/dist/web/assets/DrawerContent-ChXgZ1Xm.js +1 -0
- package/dist/web/assets/FavoritesDrawer-CG9eI9Ay.css +1 -0
- package/dist/web/assets/FavoritesDrawer-Cz95o4CD.js +1 -0
- package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +1 -0
- package/dist/web/assets/GatewayConvertDrawer-DLlbMz27.js +1 -0
- package/dist/web/assets/Home-CSuFuJ9Y.css +1 -0
- package/dist/web/assets/Home-D7CCOFQC.js +1 -0
- package/dist/web/assets/MarkdownEditor-qfVgSre_.css +1 -0
- package/dist/web/assets/MarkdownEditor-rEZ-c72L.js +1 -0
- package/dist/web/assets/McpDrawer-C0dnsZhQ.css +1 -0
- package/dist/web/assets/McpDrawer-DfoCyxU2.js +1 -0
- package/dist/web/assets/McpFormDrawer-BRbDcawn.css +1 -0
- package/dist/web/assets/McpFormDrawer-SKu8HNUR.js +1 -0
- package/dist/web/assets/PluginManager-BbeK-3rV.js +1 -0
- package/dist/web/assets/PluginsDrawer-CWVJ7yjS.js +1 -0
- package/dist/web/assets/PluginsDrawer-DM3QjprE.css +1 -0
- package/dist/web/assets/PluginsPanel-Cm2N3StG.js +1 -0
- package/dist/web/assets/PluginsPanel-D6ZqITcR.css +1 -0
- package/dist/web/assets/ProjectList-677RLZ8O.css +1 -0
- package/dist/web/assets/ProjectList-CsjnsNPv.js +1 -0
- package/dist/web/assets/PromptsDrawer-BIgiYCEQ.css +1 -0
- package/dist/web/assets/PromptsDrawer-EGd3djFa.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +1 -0
- package/dist/web/assets/RecentSessionsDrawer-D1vtoLAs.js +1 -0
- package/dist/web/assets/SessionCard-BgLg49U3.js +1 -0
- package/dist/web/assets/SessionCard-aM2NHu6i.css +1 -0
- package/dist/web/assets/SessionList-BpYRz1cX.css +1 -0
- package/dist/web/assets/SessionList-TRiTklWk.js +1 -0
- package/dist/web/assets/SettingsDrawer-d5liXjHq.css +1 -0
- package/dist/web/assets/SettingsDrawer-rz2WejGY.js +1 -0
- package/dist/web/assets/SkillManager-BTjTUbHq.js +1 -0
- package/dist/web/assets/SkillManager-C0GCT2Kb.css +1 -0
- package/dist/web/assets/SkillsDrawer-BCNme-aJ.css +1 -0
- package/dist/web/assets/SkillsDrawer-pN0hbkK3.js +1 -0
- package/dist/web/assets/SkillsPanel-B3Nm5j32.css +1 -0
- package/dist/web/assets/SkillsPanel-gmBWEoiF.js +1 -0
- package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +1 -0
- package/dist/web/assets/SpeedTestDrawer-DcNzuVJk.js +1 -0
- package/dist/web/assets/Upload-v5rV0qTT.js +1 -0
- package/dist/web/assets/WorkspaceDrawer-CGnMu9p1.css +1 -0
- package/dist/web/assets/WorkspaceDrawer-DxmicM4U.js +1 -0
- package/dist/web/assets/WorkspaceManager-CJHCjARN.js +1 -0
- package/dist/web/assets/WorkspacePanel-D0-zHTHP.css +1 -0
- package/dist/web/assets/WorkspacePanel-D8_Zq-du.js +1 -0
- package/dist/web/assets/_plugin-vue_export-helper-BeLZJZij.js +1 -0
- package/dist/web/assets/client-UDwXBP9i.js +1 -0
- package/dist/web/assets/clipboard-B46yyoz_.js +1 -0
- package/dist/web/assets/headers-cf-QapUq.js +1 -0
- package/dist/web/assets/icons-B2Vg-IOC.js +1 -0
- package/dist/web/assets/index-C0EHRym0.css +1 -0
- package/dist/web/assets/index-DnEocV0M.js +2 -0
- package/dist/web/assets/light-Cp0t7v32.js +1 -0
- package/dist/web/assets/markdown-u082bbtj.js +1 -0
- package/dist/web/assets/platforms-CBkB45JP.js +1 -0
- package/dist/web/assets/platforms-FTpjxAT6.js +1 -0
- package/dist/web/assets/project-config-B2baPzYL.js +1 -0
- package/dist/web/assets/sessions-Cmy31slD.js +1 -0
- package/dist/web/assets/sessions-yC_oVVh8.js +1 -0
- package/dist/web/assets/skill-repo-input-B_fkGlUK.js +1 -0
- package/dist/web/assets/use-lock-html-scroll-DXIqmUW4.js +1 -0
- package/dist/web/assets/use-message-DoLM8C1C.js +1 -0
- package/dist/web/assets/useResponsiveDrawer-B3uojyvV.js +1 -0
- package/dist/web/assets/vendors-D0c_0KLP.js +1 -0
- package/dist/web/assets/vue-vendor-BqZbbXHA.js +1 -0
- package/dist/web/assets/workspaces-mjyPVGS8.js +1 -0
- package/dist/web/assets/xml-CgY3RMlX.js +1 -0
- package/dist/web/index.html +12 -8
- package/docs/native-log-memory-2026-09-16.md +43 -0
- package/docs/superpowers/plans/2026-08-26-configuration-driven-cli-platform.md +1578 -0
- package/docs/superpowers/plans/2026-08-30-project-scoped-config-control.md +1260 -0
- package/docs/superpowers/plans/2026-09-01-omp-skill-refresh-boundary.md +213 -0
- package/docs/superpowers/plans/2026-09-01-realtime-logs-panel.md +125 -0
- package/docs/superpowers/plans/2026-09-02-config-driven-platform-boundary.md +86 -0
- package/docs/superpowers/plans/2026-09-02-platform-implementation-drivers.md +508 -0
- package/docs/superpowers/plans/2026-09-03-cli-platform-correctness-boundary.md +760 -0
- package/docs/superpowers/plans/2026-09-03-dependency-security-remediation.md +133 -0
- package/docs/superpowers/plans/2026-09-04-model-catalog-pricing-sync.md +1078 -0
- package/docs/superpowers/plans/2026-09-05-codex-project-scan-500.md +128 -0
- package/docs/superpowers/plans/2026-09-06-project-skill-path-fix.md +97 -0
- package/docs/superpowers/specs/2026-08-26-configuration-driven-cli-platform-design.md +408 -0
- package/docs/superpowers/specs/2026-08-30-project-scoped-config-control-design.md +467 -0
- package/docs/superpowers/specs/2026-09-01-claude-session-content-repair-design.md +111 -0
- package/docs/superpowers/specs/2026-09-01-omp-skill-refresh-boundary-design.md +83 -0
- package/docs/superpowers/specs/2026-09-01-realtime-logs-panel-design.md +27 -0
- package/docs/superpowers/specs/2026-09-02-config-driven-platform-boundary-design.md +64 -0
- package/docs/superpowers/specs/2026-09-02-platform-implementation-driver-boundary-design.md +123 -0
- package/docs/superpowers/specs/2026-09-03-cli-platform-correctness-boundary-design.md +249 -0
- package/docs/superpowers/specs/2026-09-03-dependency-security-remediation-design.md +87 -0
- package/docs/superpowers/specs/2026-09-04-model-catalog-pricing-sync-design.md +301 -0
- package/docs/superpowers/specs/2026-09-05-codex-project-scan-500-design.md +24 -0
- package/package.json +8 -4
- package/src/commands/channels.js +88 -191
- package/src/commands/cli-type.js +19 -11
- package/src/commands/daemon.js +94 -32
- package/src/commands/doctor.js +16 -9
- package/src/commands/export-config.js +23 -5
- package/src/commands/list.js +130 -52
- package/src/commands/logs.js +44 -27
- package/src/commands/platform-command-registry.js +78 -0
- package/src/commands/plugin.js +11 -5
- package/src/commands/port-config.js +34 -52
- package/src/commands/proxy-control.js +207 -145
- package/src/commands/proxy.js +2 -2
- package/src/commands/resume.js +87 -91
- package/src/commands/search.js +137 -67
- package/src/commands/stats.js +30 -22
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +94 -111
- package/src/commands/ui.js +67 -6
- package/src/commands/workspace.js +90 -8
- package/src/config/default.js +6 -53
- package/src/config/loader.js +61 -24
- package/src/config/model-metadata.js +49 -59
- package/src/config/model-metadata.json +4095 -99
- package/src/config/paths.js +181 -8
- package/src/index.js +134 -71
- package/src/platforms/access.js +196 -0
- package/src/{server/services → platforms}/agents-service.js +354 -197
- package/src/platforms/channel-auth-registry.js +162 -0
- package/src/platforms/channel-auth-service.js +82 -0
- package/src/{server/services → platforms}/commands-service.js +195 -115
- package/src/platforms/driver-registry.js +59 -0
- package/src/platforms/drivers/claude/api-operations.js +62 -0
- package/src/{server/services/channels.js → platforms/drivers/claude/channels-implementation.js} +131 -33
- package/src/platforms/drivers/claude/channels.js +39 -0
- package/src/{server/services/config-sync-service.js → platforms/drivers/claude/config-sync.js} +29 -5
- package/src/{server/services → platforms/drivers/claude}/health-check.js +4 -4
- package/src/platforms/drivers/claude/health.js +22 -0
- package/src/platforms/drivers/claude/mcp.js +9 -0
- package/src/{server/services/settings-manager.js → platforms/drivers/claude/native-config-implementation.js} +13 -4
- package/src/platforms/drivers/claude/native-config.js +46 -0
- package/src/platforms/drivers/claude/native-logs.js +72 -0
- package/src/{server/services/claude-openai-gateway.js → platforms/drivers/claude/openai-gateway.js} +8 -65
- package/src/platforms/drivers/claude/project-config.js +21 -0
- package/src/platforms/drivers/claude/projects.js +62 -0
- package/src/platforms/drivers/claude/prompts.js +9 -0
- package/src/{server/proxy-server.js → platforms/drivers/claude/proxy-implementation.js} +41 -278
- package/src/platforms/drivers/claude/proxy.js +16 -0
- package/src/platforms/drivers/claude/resource-sync.js +9 -0
- package/src/platforms/drivers/claude/session-history-adapter.js +313 -0
- package/src/{server/services/sessions.js → platforms/drivers/claude/sessions-implementation.js} +145 -35
- package/src/platforms/drivers/claude/sessions.js +76 -0
- package/src/{server/services/claude-statistics-service.js → platforms/drivers/claude/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/claude/statistics.js +16 -0
- package/src/{server/services/claude-wire.js → platforms/drivers/claude/wire.js} +1 -1
- package/src/platforms/drivers/codex/api-operations.js +9 -0
- package/src/{server/services/codex-channels.js → platforms/drivers/codex/channels-implementation.js} +149 -14
- package/src/platforms/drivers/codex/channels.js +45 -0
- package/src/{server/services/codex-config.js → platforms/drivers/codex/config.js} +12 -2
- package/src/{server/services/codex-env-manager.js → platforms/drivers/codex/env-manager.js} +1 -1
- package/src/platforms/drivers/codex/mcp.js +9 -0
- package/src/{server/services/codex-settings-manager.js → platforms/drivers/codex/native-config-implementation.js} +15 -6
- package/src/platforms/drivers/codex/native-config.js +47 -0
- package/src/platforms/drivers/codex/native-logs.js +166 -0
- package/src/platforms/drivers/codex/project-config.js +21 -0
- package/src/platforms/drivers/codex/projects.js +26 -0
- package/src/platforms/drivers/codex/prompts.js +9 -0
- package/src/{server/codex-proxy-server.js → platforms/drivers/codex/proxy-implementation.js} +30 -233
- package/src/platforms/drivers/codex/proxy.js +16 -0
- package/src/platforms/drivers/codex/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/codex.js → platforms/drivers/codex/session-history-adapter.js} +37 -23
- package/src/{server/services/codex-sessions.js → platforms/drivers/codex/sessions-implementation.js} +81 -75
- package/src/platforms/drivers/codex/sessions.js +36 -0
- package/src/{server/services/codex-statistics-service.js → platforms/drivers/codex/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/codex/statistics.js +16 -0
- package/src/platforms/drivers/gemini/api-operations.js +9 -0
- package/src/{server/services/gemini-channels.js → platforms/drivers/gemini/channels-implementation.js} +202 -37
- package/src/platforms/drivers/gemini/channels.js +39 -0
- package/src/{server/services/gemini-config.js → platforms/drivers/gemini/config.js} +12 -2
- package/src/platforms/drivers/gemini/mcp.js +9 -0
- package/src/{server/services/gemini-settings-manager.js → platforms/drivers/gemini/native-config-implementation.js} +14 -5
- package/src/platforms/drivers/gemini/native-config.js +39 -0
- package/src/platforms/drivers/gemini/native-logs.js +180 -0
- package/src/platforms/drivers/gemini/project-config.js +21 -0
- package/src/platforms/drivers/gemini/projects.js +26 -0
- package/src/platforms/drivers/gemini/prompts.js +9 -0
- package/src/{server/gemini-proxy-server.js → platforms/drivers/gemini/proxy-implementation.js} +36 -267
- package/src/platforms/drivers/gemini/proxy.js +16 -0
- package/src/platforms/drivers/gemini/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/gemini.js → platforms/drivers/gemini/session-history-adapter.js} +70 -18
- package/src/{server/services/gemini-sessions.js → platforms/drivers/gemini/sessions-implementation.js} +53 -37
- package/src/platforms/drivers/gemini/sessions.js +47 -0
- package/src/{server/services/gemini-statistics-service.js → platforms/drivers/gemini/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/gemini/statistics.js +16 -0
- package/src/{server/services/gemini-wire.js → platforms/drivers/gemini/wire.js} +1 -1
- package/src/platforms/drivers/generic-filesystem.js +166 -0
- package/src/platforms/drivers/generic-jsonl.js +141 -0
- package/src/platforms/drivers/generic-mcp.js +44 -0
- package/src/platforms/drivers/generic-openai-compatible.js +45 -0
- package/src/platforms/drivers/generic-prompt.js +41 -0
- package/src/platforms/drivers/legacy.js +677 -0
- package/src/platforms/drivers/native-config-snapshot.js +125 -0
- package/src/platforms/drivers/native-log-utils.js +1292 -0
- package/src/platforms/drivers/notification-hooks.js +578 -0
- package/src/{server/api/omp-config.js → platforms/drivers/omp/api-operations.js} +52 -64
- package/src/{server/services/omp-auth-provider-worker.js → platforms/drivers/omp/auth-provider-worker.js} +1 -1
- package/src/{server/services/omp-auth-providers.js → platforms/drivers/omp/auth-providers.js} +1 -1
- package/src/{server/services/omp-channels.js → platforms/drivers/omp/channels-implementation.js} +166 -39
- package/src/platforms/drivers/omp/channels.js +58 -0
- package/src/{server/services/omp-config.js → platforms/drivers/omp/config.js} +53 -13
- package/src/{server/services/omp-gateway-routing.js → platforms/drivers/omp/gateway-routing.js} +14 -7
- package/src/{server/services/omp-gateway.js → platforms/drivers/omp/gateway.js} +4 -262
- package/src/platforms/drivers/omp/mcp.js +9 -0
- package/src/{server/services/omp-settings-manager.js → platforms/drivers/omp/native-config-implementation.js} +75 -10
- package/src/platforms/drivers/omp/native-config.js +46 -0
- package/src/platforms/drivers/omp/native-logs.js +52 -0
- package/src/{server/services/omp-native-plugin-adapter.js → platforms/drivers/omp/native-plugin-adapter.js} +91 -4
- package/src/platforms/drivers/omp/project-config.js +29 -0
- package/src/platforms/drivers/omp/projects.js +20 -0
- package/src/platforms/drivers/omp/prompts.js +9 -0
- package/src/{server/omp-proxy-server.js → platforms/drivers/omp/proxy-implementation.js} +6 -15
- package/src/platforms/drivers/omp/proxy.js +16 -0
- package/src/platforms/drivers/omp/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/omp.js → platforms/drivers/omp/session-history-adapter.js} +42 -18
- package/src/{server/services/omp-sessions.js → platforms/drivers/omp/sessions-implementation.js} +130 -94
- package/src/platforms/drivers/omp/sessions.js +36 -0
- package/src/{server/services/omp-skill-discovery.js → platforms/drivers/omp/skill-discovery.js} +159 -32
- package/src/{server/services/omp-skill-settings-service.js → platforms/drivers/omp/skill-settings.js} +1 -1
- package/src/{server/services/omp-statistics-service.js → platforms/drivers/omp/statistics-implementation.js} +3 -3
- package/src/platforms/drivers/omp/statistics.js +16 -0
- package/src/platforms/drivers/opencode/api-operations.js +9 -0
- package/src/{server/services/opencode-channels.js → platforms/drivers/opencode/channels-implementation.js} +28 -9
- package/src/platforms/drivers/opencode/channels.js +40 -0
- package/src/platforms/drivers/opencode/conversion.js +41 -0
- package/src/{server/services/opencode-gateway-converter.js → platforms/drivers/opencode/gateway-converter.js} +1 -1
- package/src/platforms/drivers/opencode/mcp.js +9 -0
- package/src/{server/services/opencode-settings-manager.js → platforms/drivers/opencode/native-config-implementation.js} +17 -6
- package/src/platforms/drivers/opencode/native-config.js +26 -0
- package/src/platforms/drivers/opencode/native-logs.js +133 -0
- package/src/platforms/drivers/opencode/project-config.js +98 -0
- package/src/platforms/drivers/opencode/projects.js +20 -0
- package/src/platforms/drivers/opencode/prompts.js +9 -0
- package/src/{server/opencode-proxy-server.js → platforms/drivers/opencode/proxy-implementation.js} +27 -362
- package/src/platforms/drivers/opencode/proxy.js +16 -0
- package/src/platforms/drivers/opencode/resource-sync.js +9 -0
- package/src/{server/services/opencode-sessions.js → platforms/drivers/opencode/sessions-implementation.js} +211 -64
- package/src/platforms/drivers/opencode/sessions.js +48 -0
- package/src/{server/services/opencode-statistics-service.js → platforms/drivers/opencode/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/opencode/statistics.js +16 -0
- package/src/platforms/drivers/secure-file-driver.js +272 -0
- package/src/platforms/drivers/unsupported.js +7 -0
- package/src/platforms/manifest-schema.js +398 -0
- package/src/platforms/manifests/claude.json +489 -0
- package/src/platforms/manifests/codex.json +448 -0
- package/src/platforms/manifests/gemini.json +447 -0
- package/src/platforms/manifests/omp.json +496 -0
- package/src/platforms/manifests/opencode.json +422 -0
- package/src/{server/services → platforms}/native-oauth-adapters.js +87 -417
- package/src/{server/services → platforms}/notification-hooks.js +140 -1023
- package/src/platforms/oauth-credentials-service.js +506 -0
- package/src/platforms/path-resolver.js +179 -0
- package/src/platforms/platform-path-context.js +305 -0
- package/src/platforms/registry.js +248 -0
- package/src/platforms/remote-notification-providers.js +678 -0
- package/src/platforms/runtime.js +259 -0
- package/src/{server/services/session-history-adapters/index.js → platforms/session-history-adapters.js} +8 -1
- package/src/plugins/plugin-loader.js +2 -2
- package/src/plugins/plugin-manager.js +2 -2
- package/src/plugins/registry.js +27 -10
- package/src/reset-config.js +21 -5
- package/src/server/api/agents.js +46 -72
- package/src/server/api/commands.js +16 -6
- package/src/server/api/config-registry.js +206 -59
- package/src/server/api/config-sync.js +3 -2
- package/src/server/api/config-templates.js +20 -4
- package/src/server/api/config.js +111 -102
- package/src/server/api/convert.js +54 -178
- package/src/server/api/dashboard.js +168 -102
- package/src/server/api/hooks.js +1 -1
- package/src/server/api/mcp.js +224 -84
- package/src/server/api/platform-api-config.js +102 -0
- package/src/server/api/platform-route-factory.js +215 -0
- package/src/server/api/platform-routes.js +26 -0
- package/src/server/api/platforms.js +110 -0
- package/src/server/api/plugins.js +57 -57
- package/src/server/api/project-config.js +168 -0
- package/src/server/api/prompts.js +58 -45
- package/src/server/api/settings.js +55 -8
- package/src/server/api/skills.js +262 -133
- package/src/server/api/ui-config.js +2 -2
- package/src/server/api/validation-errors.js +18 -4
- package/src/server/api/workspaces.js +47 -12
- package/src/server/index.js +164 -213
- package/src/server/platform-context.js +81 -0
- package/src/server/services/channel-balance.js +11 -20
- package/src/server/services/channel-health.js +21 -6
- package/src/server/services/channel-scheduler.js +29 -39
- package/src/server/services/channel-sync-utils.js +23 -13
- package/src/server/services/config-export-service.js +350 -319
- package/src/server/services/config-registry-service.js +280 -54
- package/src/server/services/config-sync-manager.js +172 -944
- package/src/server/services/config-templates-service.js +419 -353
- package/src/server/services/control-manifest-store.js +206 -0
- package/src/server/services/dashboard-snapshot-worker.js +656 -100
- package/src/server/services/effective-control-service.js +557 -0
- package/src/server/services/local-resource-index.js +314 -0
- package/src/server/services/mcp-service.js +607 -977
- package/src/server/services/model-detector.js +15 -10
- package/src/server/services/native-log-channel-resolver.js +214 -0
- package/src/server/services/native-log-observer.js +291 -0
- package/src/server/services/network-access.js +1 -1
- package/src/server/services/platform-catalog.js +82 -0
- package/src/server/services/platform-resolution.js +32 -29
- package/src/server/services/plugins-service.js +376 -122
- package/src/server/services/project-config-service.js +550 -0
- package/src/server/services/project-path-validation.js +31 -0
- package/src/server/services/prompts-service.js +267 -233
- package/src/server/services/proxy-runtime.js +7 -4
- package/src/server/services/remote-credential-cache.js +43 -0
- package/src/server/services/repo-scanner-base.js +182 -75
- package/src/server/services/request-logger.js +7 -4
- package/src/server/services/session-history-index.js +780 -213
- package/src/server/services/session-history-worker.js +228 -33
- package/src/server/services/session-snapshot-worker.js +124 -34
- package/src/server/services/session-snapshots.js +4 -2
- package/src/server/services/skill-artifact-store.js +355 -0
- package/src/server/services/skill-format-adapters.js +65 -0
- package/src/server/services/skill-projection-service.js +323 -0
- package/src/server/services/skill-refresh-task-service.js +275 -0
- package/src/server/services/skill-service.js +2020 -677
- package/src/server/services/snapshot-cache.js +30 -8
- package/src/server/services/speed-test.js +8 -8
- package/src/server/services/sqlite-connection.js +19 -3
- package/src/server/services/statistics-service.js +240 -57
- package/src/server/services/ui-config.js +107 -76
- package/src/server/services/usage-log-utils.js +98 -0
- package/src/server/services/web-build.js +1 -1
- package/src/server/services/workspace-service.js +59 -29
- package/src/server/utils/pricing.js +10 -19
- package/src/server/websocket-server.js +153 -28
- package/src/{server/services/base → shared}/base-channel-service.js +156 -28
- package/src/shared/capability-driver.js +104 -0
- package/src/shared/channel-auth-policy.js +27 -0
- package/src/shared/channel-driver.js +161 -0
- package/src/shared/driver-factories/api.js +180 -0
- package/src/shared/driver-factories/mcp.js +244 -0
- package/src/shared/driver-factories/projects.js +121 -0
- package/src/shared/driver-factories/prompts.js +58 -0
- package/src/shared/driver-factories/resource-sync.js +375 -0
- package/src/shared/driver-result.js +78 -0
- package/src/shared/mcp-format.js +189 -0
- package/src/shared/platforms.js +104 -167
- package/src/shared/project-config.js +512 -0
- package/src/shared/proxy-driver.js +57 -0
- package/src/{server/services/base → shared}/response-usage-parser.js +8 -4
- package/src/ui/menu.js +91 -67
- package/src/utils/port-helper.js +35 -19
- package/src/utils/session.js +1 -1
- package/dist/web/assets/Analytics-COEUXmd4.js +0 -25
- package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
- package/dist/web/assets/ConfigTemplates-DLLkS53H.js +0 -1
- package/dist/web/assets/Home-DCEyLzwX.js +0 -1
- package/dist/web/assets/Home-DYVy4cGZ.css +0 -1
- package/dist/web/assets/PluginManager-Oe1ZHoMO.js +0 -1
- package/dist/web/assets/ProjectList-CWjuAFof.js +0 -1
- package/dist/web/assets/ProjectList-VeMNXfgO.css +0 -1
- package/dist/web/assets/SessionList-C4chCPxK.css +0 -1
- package/dist/web/assets/SessionList-v3ehGg9U.js +0 -1
- package/dist/web/assets/SkillManager-BsX8Qpj0.css +0 -1
- package/dist/web/assets/SkillManager-DUF_qP5S.js +0 -1
- package/dist/web/assets/WorkspaceManager-CjAWBosO.js +0 -1
- package/dist/web/assets/icons-nGdSKQn2.js +0 -1
- package/dist/web/assets/index-D5hyHPj7.js +0 -2
- package/dist/web/assets/index-bJRG3RXH.css +0 -1
- package/dist/web/assets/markdown-BNJfEgCA.js +0 -1
- package/dist/web/assets/markdown-BfC0goYb.css +0 -10
- package/dist/web/assets/naive-ui-BALVpMgd.js +0 -1
- package/dist/web/assets/vendors-DQA4Hw9v.js +0 -7
- package/dist/web/assets/vue-vendor-B5RqjEjQ.js +0 -45
- package/src/config/model-pricing.js +0 -35
- package/src/server/api/channels.js +0 -439
- package/src/server/api/claude-hooks.js +0 -56
- package/src/server/api/claude-statistics.js +0 -57
- package/src/server/api/codex-channels.js +0 -491
- package/src/server/api/codex-projects.js +0 -117
- package/src/server/api/codex-proxy.js +0 -293
- package/src/server/api/codex-sessions.js +0 -652
- package/src/server/api/codex-statistics.js +0 -57
- package/src/server/api/gemini-channels.js +0 -392
- package/src/server/api/gemini-projects.js +0 -103
- package/src/server/api/gemini-proxy.js +0 -231
- package/src/server/api/gemini-sessions.js +0 -550
- package/src/server/api/gemini-statistics.js +0 -57
- package/src/server/api/health-check.js +0 -31
- package/src/server/api/oauth-credentials.js +0 -198
- package/src/server/api/omp-channels.js +0 -575
- package/src/server/api/omp-projects.js +0 -88
- package/src/server/api/omp-proxy.js +0 -125
- package/src/server/api/omp-sessions.js +0 -386
- package/src/server/api/omp-statistics.js +0 -40
- package/src/server/api/opencode-channels.js +0 -471
- package/src/server/api/opencode-projects.js +0 -110
- package/src/server/api/opencode-proxy.js +0 -354
- package/src/server/api/opencode-sessions.js +0 -466
- package/src/server/api/opencode-statistics.js +0 -57
- package/src/server/api/projects.js +0 -120
- package/src/server/api/proxy.js +0 -330
- package/src/server/api/sessions.js +0 -810
- package/src/server/services/oauth-credentials-service.js +0 -945
- package/src/server/services/omp-session-log-observer.js +0 -138
- package/src/server/services/opencode-gateway-adapters.js +0 -43
- package/src/server/services/project-snapshots.js +0 -59
- package/src/server/services/proxy-log-helper.js +0 -298
- package/src/server/services/session-cache.js +0 -127
- package/src/server/services/session-converter.js +0 -582
- package/src/server/services/session-history-adapters/claude.js +0 -235
- /package/src/{server/services/claude-token-recovery.js → platforms/drivers/claude/token-recovery.js} +0 -0
- /package/src/{server/services/codex-parser.js → platforms/drivers/codex/parser.js} +0 -0
- /package/src/{server/services/codex-speed-test-template.json → platforms/drivers/codex/speed-test-template.json} +0 -0
- /package/src/{server/services/codex-wire.js → platforms/drivers/codex/wire.js} +0 -0
- /package/src/{server/services/omp-auth-gateway-client.js → platforms/drivers/omp/auth-gateway-client.js} +0 -0
- /package/src/{server/services/opencode-normalization.js → platforms/drivers/opencode/normalization.js} +0 -0
- /package/src/{server/services → shared}/config-artifact-paths.js +0 -0
- /package/src/{server/services/base → shared}/proxy-utils.js +0 -0
|
@@ -7,31 +7,57 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const os = require('os');
|
|
11
10
|
const https = require('https');
|
|
12
11
|
const http = require('http');
|
|
13
12
|
const { execFileSync } = require('child_process');
|
|
14
|
-
const {
|
|
15
|
-
const { pipeline } = require('stream/promises');
|
|
16
|
-
const AdmZip = require('adm-zip');
|
|
13
|
+
const { randomUUID } = require('crypto');
|
|
17
14
|
const {
|
|
18
15
|
parseSkillContent,
|
|
19
16
|
} = require('./format-converter');
|
|
20
17
|
const { maskToken } = require('./oauth-utils');
|
|
21
|
-
const
|
|
22
|
-
const {
|
|
23
|
-
const
|
|
18
|
+
const pathsModule = require('../../config/paths');
|
|
19
|
+
const { NATIVE_PATHS, HOME_DIR, PATHS } = pathsModule;
|
|
20
|
+
const getPlatformStatePaths = typeof pathsModule.getPlatformStatePaths === 'function'
|
|
21
|
+
? pathsModule.getPlatformStatePaths
|
|
22
|
+
: platform => ({
|
|
23
|
+
localSkills: PATHS.localSkills?.[platform],
|
|
24
|
+
skillRepos: PATHS.skillRepos?.[platform],
|
|
25
|
+
skillCaches: PATHS.skillCaches?.[platform]
|
|
26
|
+
});
|
|
27
|
+
const { getOmpPaths } = require('../../platforms/drivers/omp/config');
|
|
28
|
+
const { discoverOmpSkills } = require('../../platforms/drivers/omp/skill-discovery');
|
|
24
29
|
const { migratePiStorage } = require('./pi-omp-migration');
|
|
25
30
|
const { resolveManagedPlatform } = require('./platform-resolution');
|
|
26
31
|
const {
|
|
27
32
|
normalizeSafeRelativePath,
|
|
28
33
|
pathHasProtectedSegment,
|
|
29
34
|
resolveInsideRoot
|
|
30
|
-
} = require('
|
|
35
|
+
} = require('../../shared/config-artifact-paths');
|
|
36
|
+
const remoteCredentialCache = require('./remote-credential-cache');
|
|
37
|
+
const { getPlatformContext } = require('../platform-context');
|
|
38
|
+
const {
|
|
39
|
+
assertExistingProjectRoot,
|
|
40
|
+
assertNoSymlinkComponents,
|
|
41
|
+
resolveProjectTarget
|
|
42
|
+
} = require('../../shared/project-config');
|
|
43
|
+
const { ControlManifestStore } = require('./control-manifest-store');
|
|
44
|
+
const { EffectiveControlService } = require('./effective-control-service');
|
|
45
|
+
const { SkillArtifactStore } = require('./skill-artifact-store');
|
|
46
|
+
const { SkillFormatAdapter } = require('./skill-format-adapters');
|
|
47
|
+
const { SkillProjectionService } = require('./skill-projection-service');
|
|
31
48
|
|
|
32
49
|
const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
|
|
33
50
|
const DEFAULT_GITHUB_HOST = 'https://github.com';
|
|
34
51
|
const DEFAULT_GITLAB_HOST = 'https://gitlab.com';
|
|
52
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
53
|
+
|
|
54
|
+
function sanitizeRefreshError(value) {
|
|
55
|
+
return String(value || '')
|
|
56
|
+
.replace(/(https?:\/\/[^\s?]+)\?[^\s]+/gi, '$1?[REDACTED]')
|
|
57
|
+
.replace(/(Bearer\s+)[^\s]+/gi, '$1[REDACTED]')
|
|
58
|
+
.replace(/([?&](?:access[_-]?token|api[-_]?key|token|password|secret|key)=)[^&\s]+/gi, '$1[REDACTED]')
|
|
59
|
+
.replace(/((?:token|secret|password|api[-_]?key|authorization)\s*[:=]\s*)[^,\s}]+/gi, '$1[REDACTED]');
|
|
60
|
+
}
|
|
35
61
|
|
|
36
62
|
function cloneRepos(repos = []) {
|
|
37
63
|
return repos.map(repo => ({ ...repo }));
|
|
@@ -98,6 +124,25 @@ function resolveLocalRepoPath(input = '') {
|
|
|
98
124
|
return path.resolve(expanded);
|
|
99
125
|
}
|
|
100
126
|
|
|
127
|
+
function resolveExistingLocalRepoRoot(input) {
|
|
128
|
+
const value = String(input || '').trim();
|
|
129
|
+
if (!value) throw new Error('Missing local repository path');
|
|
130
|
+
const resolved = path.resolve(value);
|
|
131
|
+
let stat;
|
|
132
|
+
try {
|
|
133
|
+
stat = fs.lstatSync(resolved);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new Error(`Local repo path not found: ${resolved}`, { cause: error });
|
|
136
|
+
}
|
|
137
|
+
if (stat.isSymbolicLink()) {
|
|
138
|
+
throw new Error(`Local repo root contains symlink: ${resolved}`);
|
|
139
|
+
}
|
|
140
|
+
if (!stat.isDirectory()) {
|
|
141
|
+
throw new Error(`Local repo path is not a directory: ${resolved}`);
|
|
142
|
+
}
|
|
143
|
+
return fs.realpathSync(resolved);
|
|
144
|
+
}
|
|
145
|
+
|
|
101
146
|
function normalizeRepoHost(host, provider = 'github') {
|
|
102
147
|
const fallback = provider === 'gitlab' ? DEFAULT_GITLAB_HOST : DEFAULT_GITHUB_HOST;
|
|
103
148
|
let normalized = String(host || '').trim();
|
|
@@ -145,6 +190,21 @@ function buildRepoLabel(repo) {
|
|
|
145
190
|
return [repo.owner, repo.name].filter(Boolean).join('/');
|
|
146
191
|
}
|
|
147
192
|
|
|
193
|
+
function sanitizeRepoUrl(value, fallback = '') {
|
|
194
|
+
const raw = String(value || '').trim();
|
|
195
|
+
if (!raw) return fallback;
|
|
196
|
+
try {
|
|
197
|
+
const parsed = new URL(raw);
|
|
198
|
+
parsed.username = '';
|
|
199
|
+
parsed.password = '';
|
|
200
|
+
parsed.search = '';
|
|
201
|
+
parsed.hash = '';
|
|
202
|
+
return parsed.toString().replace(/\/$/, '');
|
|
203
|
+
} catch {
|
|
204
|
+
return fallback;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
148
208
|
function buildRepoId(repo) {
|
|
149
209
|
const directory = normalizeRepoDirectory(repo.directory);
|
|
150
210
|
const branch = String(repo.branch || 'main').trim() || 'main';
|
|
@@ -214,11 +274,17 @@ const PLATFORM_CONFIG = {
|
|
|
214
274
|
}
|
|
215
275
|
};
|
|
216
276
|
|
|
217
|
-
// 缓存有效期(5分钟)
|
|
218
|
-
const CACHE_TTL = 5 * 60 * 1000;
|
|
219
|
-
|
|
220
277
|
class SkillService {
|
|
221
|
-
constructor(
|
|
278
|
+
constructor(
|
|
279
|
+
platform = 'claude',
|
|
280
|
+
{
|
|
281
|
+
registry = getPlatformContext().registry,
|
|
282
|
+
controlService = null,
|
|
283
|
+
artifactStore = null,
|
|
284
|
+
formatAdapter = null
|
|
285
|
+
} = {}
|
|
286
|
+
) {
|
|
287
|
+
this.registry = registry;
|
|
222
288
|
this.platform = resolveManagedPlatform(platform).platform;
|
|
223
289
|
if (this.platform === 'omp') {
|
|
224
290
|
const migration = migratePiStorage(PATHS);
|
|
@@ -228,17 +294,55 @@ class SkillService {
|
|
|
228
294
|
}
|
|
229
295
|
this.configDir = PATHS.config;
|
|
230
296
|
|
|
231
|
-
|
|
232
|
-
|
|
297
|
+
this.pathContext = null;
|
|
298
|
+
try {
|
|
299
|
+
if (typeof this.registry?.resolvePathContext === 'function') {
|
|
300
|
+
this.pathContext = this.registry.resolvePathContext(this.platform);
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
this.pathContext = null;
|
|
304
|
+
}
|
|
305
|
+
const pathContext = this.pathContext?.customized ? this.pathContext : null;
|
|
306
|
+
const native = pathContext?.native || NATIVE_PATHS[this.platform] || {};
|
|
307
|
+
const state = pathContext?.state || getPlatformStatePaths(this.platform);
|
|
308
|
+
const platformConfig = PLATFORM_CONFIG[this.platform] || {};
|
|
309
|
+
this.installDir = this.platform === 'omp' && !this.pathContext?.customized
|
|
233
310
|
? getOmpPaths().skills
|
|
234
|
-
:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
311
|
+
: native.skills
|
|
312
|
+
|| (native.dir ? path.join(native.dir, 'skills') : null)
|
|
313
|
+
|| platformConfig.installDir
|
|
314
|
+
|| path.join(this.configDir, 'native', this.platform, 'skills');
|
|
315
|
+
this.storageDir = state.localSkills || platformConfig.storageDir;
|
|
316
|
+
this.reposConfigPath = state.skillRepos || platformConfig.reposFile;
|
|
317
|
+
this.cachePath = state.skillCaches || platformConfig.cacheFile;
|
|
318
|
+
this.artifactStore = artifactStore || (
|
|
319
|
+
PATHS.skillArtifacts
|
|
320
|
+
? new SkillArtifactStore({ root: PATHS.skillArtifacts })
|
|
321
|
+
: null
|
|
322
|
+
);
|
|
323
|
+
this.formatAdapter = formatAdapter || new SkillFormatAdapter();
|
|
324
|
+
this.controlService = controlService || (
|
|
325
|
+
PATHS.effectiveControlManifest
|
|
326
|
+
? new EffectiveControlService({
|
|
327
|
+
store: new ControlManifestStore({
|
|
328
|
+
userPath: PATHS.effectiveControlManifest,
|
|
329
|
+
projectPathResolver: ({ projectPath }) => path.join(projectPath, '.ctx-control.json')
|
|
330
|
+
}),
|
|
331
|
+
projection: new SkillProjectionService({ registry: this.registry })
|
|
332
|
+
})
|
|
333
|
+
: null
|
|
334
|
+
);
|
|
238
335
|
|
|
239
|
-
//
|
|
336
|
+
// Prepared projections are keyed by scope and canonical cwd; raw repository skills are cwd-independent.
|
|
240
337
|
this.skillsCache = null;
|
|
241
338
|
this.cacheTime = 0;
|
|
339
|
+
this._preparedSkillsCache = new Map();
|
|
340
|
+
this._cacheGeneration = 0;
|
|
341
|
+
this._remoteSkillsCache = null;
|
|
342
|
+
this._remoteSkillsFetchedAt = 0;
|
|
343
|
+
this._legacyMigrationChecked = false;
|
|
344
|
+
|
|
345
|
+
this.githubTokenCache = new Map();
|
|
242
346
|
|
|
243
347
|
// 确保目录存在
|
|
244
348
|
this.ensureDirs();
|
|
@@ -246,7 +350,20 @@ class SkillService {
|
|
|
246
350
|
|
|
247
351
|
refreshOmpPaths() {
|
|
248
352
|
if (this.platform !== 'omp') return;
|
|
249
|
-
|
|
353
|
+
let nextContext = null;
|
|
354
|
+
try {
|
|
355
|
+
if (typeof this.registry?.resolvePathContext === 'function') {
|
|
356
|
+
nextContext = this.registry.resolvePathContext(this.platform);
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
nextContext = null;
|
|
360
|
+
}
|
|
361
|
+
const nextInstallDir = nextContext?.customized
|
|
362
|
+
? (nextContext.native?.skills || getOmpPaths().skills)
|
|
363
|
+
: nextContext?.paths?.skills
|
|
364
|
+
? nextContext.native?.skills
|
|
365
|
+
: getOmpPaths().skills;
|
|
366
|
+
this.pathContext = nextContext || this.pathContext;
|
|
250
367
|
if (this.installDir !== nextInstallDir) {
|
|
251
368
|
this.installDir = nextInstallDir;
|
|
252
369
|
this.clearCache();
|
|
@@ -271,11 +388,21 @@ class SkillService {
|
|
|
271
388
|
if (!fs.existsSync(cacheDir)) {
|
|
272
389
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
273
390
|
}
|
|
391
|
+
if (this.artifactStore?.root && !fs.existsSync(this.artifactStore.root)) {
|
|
392
|
+
fs.mkdirSync(this.artifactStore.root, { recursive: true });
|
|
393
|
+
}
|
|
274
394
|
}
|
|
275
395
|
|
|
276
396
|
clearCache({ removeFile = false } = {}) {
|
|
397
|
+
this._cacheGeneration++;
|
|
277
398
|
this.skillsCache = null;
|
|
278
399
|
this.cacheTime = 0;
|
|
400
|
+
this._preparedSkillsCache.clear();
|
|
401
|
+
this._remoteSkillsCache = null;
|
|
402
|
+
this._remoteSkillsFetchedAt = 0;
|
|
403
|
+
this.githubTokenCache.clear();
|
|
404
|
+
remoteCredentialCache.clear('github');
|
|
405
|
+
remoteCredentialCache.clear('gitlab');
|
|
279
406
|
|
|
280
407
|
if (removeFile) {
|
|
281
408
|
try {
|
|
@@ -288,6 +415,588 @@ class SkillService {
|
|
|
288
415
|
}
|
|
289
416
|
}
|
|
290
417
|
|
|
418
|
+
_skillCwdKey(options = {}) {
|
|
419
|
+
if (!options.cwd) return '';
|
|
420
|
+
const resolved = path.resolve(options.cwd);
|
|
421
|
+
try {
|
|
422
|
+
return fs.realpathSync(resolved);
|
|
423
|
+
} catch {
|
|
424
|
+
return resolved;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_skillCacheKey(options = {}) {
|
|
429
|
+
const scope = options.scope || 'user';
|
|
430
|
+
return `${scope}:${this._skillCwdKey(options)}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
_storePrepared(cacheKey, skills, generation = this._cacheGeneration) {
|
|
435
|
+
const value = Array.isArray(skills) ? skills.map(skill => ({ ...skill })) : [];
|
|
436
|
+
if (generation !== this._cacheGeneration) {
|
|
437
|
+
return value.map(skill => ({ ...skill }));
|
|
438
|
+
}
|
|
439
|
+
const entry = { value, cachedAt: Date.now(), generation };
|
|
440
|
+
this._preparedSkillsCache.set(cacheKey, entry);
|
|
441
|
+
if (cacheKey === 'user:') {
|
|
442
|
+
this.skillsCache = value;
|
|
443
|
+
this.cacheTime = entry.cachedAt;
|
|
444
|
+
}
|
|
445
|
+
return value.map(skill => ({ ...skill }));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
_artifactSkills(options = {}) {
|
|
449
|
+
if (!this.artifactStore || typeof this.artifactStore.list !== 'function') return [];
|
|
450
|
+
const artifacts = this.artifactStore.list({ platform: this.platform });
|
|
451
|
+
const projectPath = options.scope === 'project' ? this._skillCwdKey(options) : null;
|
|
452
|
+
return artifacts.map(artifact => {
|
|
453
|
+
if (!artifact || typeof artifact.root !== 'string') return null;
|
|
454
|
+
const artifactScope = artifact.sourceScope || 'user';
|
|
455
|
+
if (
|
|
456
|
+
(artifactScope === 'project' && (!projectPath || artifact.projectPath !== projectPath))
|
|
457
|
+
|| (artifactScope !== 'project' && artifact.projectPath && artifact.projectPath !== projectPath)
|
|
458
|
+
) return null;
|
|
459
|
+
const skillPath = path.join(artifact.root, 'SKILL.md');
|
|
460
|
+
try {
|
|
461
|
+
if (!fs.existsSync(skillPath) || fs.lstatSync(skillPath).isSymbolicLink()) return null;
|
|
462
|
+
const metadata = this.parseSkillMd(fs.readFileSync(skillPath, 'utf8'));
|
|
463
|
+
const rawSourceProvider = artifact.sourceProvider || 'remote';
|
|
464
|
+
const sourceProvider = rawSourceProvider === 'remote'
|
|
465
|
+
? (artifact.repoProvider || 'remote')
|
|
466
|
+
: rawSourceProvider;
|
|
467
|
+
const source = rawSourceProvider === 'remote'
|
|
468
|
+
? 'remote'
|
|
469
|
+
: (rawSourceProvider === 'local-repo' ? 'local-repo' : 'provider-installed');
|
|
470
|
+
const directory = artifact.targetDirectory
|
|
471
|
+
|| artifact.directory
|
|
472
|
+
|| artifact.fullDirectory
|
|
473
|
+
|| metadata.name;
|
|
474
|
+
return {
|
|
475
|
+
key: `artifact:${this.platform}:${artifact.sourceKey}:${artifact.format}`,
|
|
476
|
+
sourceKey: artifact.sourceKey,
|
|
477
|
+
name: metadata.name || artifact.name || directory,
|
|
478
|
+
description: metadata.description || artifact.description || '',
|
|
479
|
+
directory,
|
|
480
|
+
fullDirectory: artifact.fullDirectory || directory,
|
|
481
|
+
installed: false,
|
|
482
|
+
cached: true,
|
|
483
|
+
isLocal: ['local', 'local-repo', 'template'].includes(sourceProvider),
|
|
484
|
+
source,
|
|
485
|
+
sourceProvider,
|
|
486
|
+
sourceScope: artifactScope,
|
|
487
|
+
projectPath: artifact.projectPath || null,
|
|
488
|
+
sourcePath: skillPath,
|
|
489
|
+
artifact: {
|
|
490
|
+
root: artifact.root,
|
|
491
|
+
contentHash: artifact.contentHash || null,
|
|
492
|
+
format: artifact.format || null,
|
|
493
|
+
state: artifact.state || 'ready',
|
|
494
|
+
fetchedAt: artifact.fetchedAt || null
|
|
495
|
+
},
|
|
496
|
+
repoProvider: artifact.repoProvider || null,
|
|
497
|
+
repoOwner: artifact.repoOwner || null,
|
|
498
|
+
repoName: artifact.repoName || null,
|
|
499
|
+
repoBranch: artifact.repoBranch || null,
|
|
500
|
+
repoDirectory: artifact.repoDirectory || artifact.fullDirectory || directory,
|
|
501
|
+
repoHost: artifact.repoHost || null,
|
|
502
|
+
repoProjectPath: artifact.repoProjectPath || null,
|
|
503
|
+
repoLocalPath: artifact.repoLocalPath || null,
|
|
504
|
+
repoUrl: artifact.repoUrl || null,
|
|
505
|
+
protected: false,
|
|
506
|
+
shadowedSources: [],
|
|
507
|
+
readmeUrl: artifact.readmeUrl || null,
|
|
508
|
+
license: metadata.license || null
|
|
509
|
+
};
|
|
510
|
+
} catch (error) {
|
|
511
|
+
console.warn(`[SkillService] Read artifact ${artifact.sourceKey || ''} error:`, error.message);
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
}).filter(Boolean);
|
|
515
|
+
}
|
|
516
|
+
_legacyCachedSkills(options = {}) {
|
|
517
|
+
const cached = this._readRawRemoteCache();
|
|
518
|
+
if (!cached || !Array.isArray(cached.skills)) return [];
|
|
519
|
+
const scope = options.scope || 'user';
|
|
520
|
+
const projectPath = scope === 'project' ? this._skillCwdKey(options) : null;
|
|
521
|
+
return cached.skills
|
|
522
|
+
.filter(skill => {
|
|
523
|
+
const skillScope = skill.sourceScope || 'user';
|
|
524
|
+
if (skillScope === 'project') {
|
|
525
|
+
return scope === 'project' && skill.projectPath === projectPath;
|
|
526
|
+
}
|
|
527
|
+
return !skill.projectPath || skill.projectPath === projectPath;
|
|
528
|
+
})
|
|
529
|
+
.map(skill => ({
|
|
530
|
+
...skill,
|
|
531
|
+
source: skill.source || 'remote',
|
|
532
|
+
sourceProvider: skill.sourceProvider || 'remote',
|
|
533
|
+
sourceScope: skill.sourceScope || 'user',
|
|
534
|
+
sourceKey: skill.sourceKey || `legacy-cache:${this.platform}:${skill.repoId || skill.repoName || ''}:${skill.fullDirectory || skill.directory || skill.name || ''}`,
|
|
535
|
+
cached: false,
|
|
536
|
+
artifactRoot: null,
|
|
537
|
+
artifactState: 'metadata_only',
|
|
538
|
+
artifact: {
|
|
539
|
+
...(skill.artifact || {}),
|
|
540
|
+
root: null,
|
|
541
|
+
state: 'metadata_only'
|
|
542
|
+
}
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
_sourceKeyForSkill(skill = {}, options = {}) {
|
|
547
|
+
const sourceProvider = skill.sourceProvider || skill.source || 'native';
|
|
548
|
+
const isProjectLocal = options.scope === 'project'
|
|
549
|
+
&& skill.sourceScope === 'project'
|
|
550
|
+
&& sourceProvider !== 'remote'
|
|
551
|
+
&& !skill.repoId;
|
|
552
|
+
const projectPath = isProjectLocal ? this._skillCwdKey(options) : '';
|
|
553
|
+
const addProjectIdentity = sourceKey => (
|
|
554
|
+
isProjectLocal && projectPath && !sourceKey.includes(':project:')
|
|
555
|
+
? `${sourceKey}:project:${projectPath}`
|
|
556
|
+
: sourceKey
|
|
557
|
+
);
|
|
558
|
+
if (skill.sourceKey) return addProjectIdentity(String(skill.sourceKey));
|
|
559
|
+
const directory = skill.fullDirectory || skill.directory || skill.name || '';
|
|
560
|
+
let sourceKey;
|
|
561
|
+
if (sourceProvider === 'native' || sourceProvider === 'native-installed') {
|
|
562
|
+
sourceKey = `native:${this.platform}:${directory}`;
|
|
563
|
+
} else if (sourceProvider === 'cc-tool' || sourceProvider === 'local') {
|
|
564
|
+
sourceKey = `local:${this.platform}:${directory}`;
|
|
565
|
+
} else {
|
|
566
|
+
const repo = skill.repoId || [skill.repoOwner, skill.repoName, skill.repoBranch].filter(Boolean).join('/');
|
|
567
|
+
sourceKey = repo
|
|
568
|
+
? `repo:${repo}:${directory}`
|
|
569
|
+
: `${sourceProvider}:${this.platform}:${skill.realPath || skill.sourcePath || directory}`;
|
|
570
|
+
}
|
|
571
|
+
return addProjectIdentity(sourceKey);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
_controlKeyForSkill(skill, scope, projectPath, sourceKey) {
|
|
575
|
+
const location = scope === 'project' ? projectPath : 'user';
|
|
576
|
+
return skill.controlKey || `skill:${this.platform}:${scope}:${location}:${sourceKey}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_materializeLocalArtifact(skill, sourceKey, options = {}) {
|
|
580
|
+
if (!this.artifactStore || !skill.sourcePath) return null;
|
|
581
|
+
if (skill.source === 'remote' || skill.sourceProvider === 'remote' || skill.artifactRoot) return null;
|
|
582
|
+
const root = path.dirname(skill.sourcePath);
|
|
583
|
+
const projectPath = skill.sourceScope === 'project' && options.scope === 'project'
|
|
584
|
+
? this._skillCwdKey(options)
|
|
585
|
+
: null;
|
|
586
|
+
try {
|
|
587
|
+
const files = this._collectLocalSkillFiles(root);
|
|
588
|
+
const normalized = this.formatAdapter.normalize({
|
|
589
|
+
platform: this.platform,
|
|
590
|
+
files,
|
|
591
|
+
sourceMetadata: {
|
|
592
|
+
name: skill.name || skill.directory,
|
|
593
|
+
sourceScope: skill.sourceScope || 'user',
|
|
594
|
+
projectPath
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
const existing = this.artifactStore.get?.({
|
|
598
|
+
platform: this.platform,
|
|
599
|
+
sourceKey,
|
|
600
|
+
format: normalized.format
|
|
601
|
+
});
|
|
602
|
+
const contentHash = this.artifactStore.hashFileTree?.(normalized.files);
|
|
603
|
+
if (existing && (!contentHash || existing.contentHash === contentHash)) return existing;
|
|
604
|
+
return this.artifactStore.publishSkill({
|
|
605
|
+
platform: this.platform,
|
|
606
|
+
sourceKey,
|
|
607
|
+
format: normalized.format,
|
|
608
|
+
files: normalized.files,
|
|
609
|
+
metadata: {
|
|
610
|
+
name: skill.name || skill.directory,
|
|
611
|
+
description: skill.description || '',
|
|
612
|
+
directory: skill.directory,
|
|
613
|
+
fullDirectory: skill.fullDirectory || skill.directory,
|
|
614
|
+
sourceProvider: skill.sourceProvider || skill.source || 'native',
|
|
615
|
+
sourceScope: skill.sourceScope || 'user',
|
|
616
|
+
...(projectPath ? { projectPath } : {}),
|
|
617
|
+
revision: skill.revision || null
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
} catch (error) {
|
|
621
|
+
console.warn(`[SkillService] Materialize local Skill ${skill.directory || ''} failed:`, error.message);
|
|
622
|
+
return {
|
|
623
|
+
root: null,
|
|
624
|
+
contentHash: null,
|
|
625
|
+
state: 'unsupported',
|
|
626
|
+
fetchedAt: null,
|
|
627
|
+
lastError: error.message
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
_skillProjectionCapability(scope) {
|
|
633
|
+
const capability = this.controlService?.projection?.getCapability?.(this.platform, scope);
|
|
634
|
+
if (capability) return capability;
|
|
635
|
+
const mapping = this.registry?.resolve?.(this.platform)?.projectResources?.skills;
|
|
636
|
+
return mapping || scope === 'user'
|
|
637
|
+
? { mode: 'native-copy', format: null }
|
|
638
|
+
: { mode: 'unsupported', format: null };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
_buildSkillControlEntry(skill, options = {}) {
|
|
642
|
+
const sourceScope = skill.sourceScope === 'project' && options.scope === 'project'
|
|
643
|
+
? 'project'
|
|
644
|
+
: 'user';
|
|
645
|
+
const projectPath = sourceScope === 'project' ? this._skillCwdKey(options) : null;
|
|
646
|
+
const sourceKey = this._sourceKeyForSkill(skill, options);
|
|
647
|
+
const controlKey = this._controlKeyForSkill(skill, sourceScope, projectPath, sourceKey);
|
|
648
|
+
const isRemote = skill.source === 'remote'
|
|
649
|
+
|| skill.sourceProvider === 'remote'
|
|
650
|
+
|| skill.artifactState === 'metadata_only'
|
|
651
|
+
|| Boolean(skill.repoId);
|
|
652
|
+
const projectionCapability = this._skillProjectionCapability(sourceScope);
|
|
653
|
+
const localArtifact = this._materializeLocalArtifact(skill, sourceKey, options);
|
|
654
|
+
const artifact = localArtifact || skill.artifact || null;
|
|
655
|
+
const artifactRoot = skill.artifactRoot || artifact?.root || (
|
|
656
|
+
skill.sourcePath ? path.dirname(skill.sourcePath) : null
|
|
657
|
+
);
|
|
658
|
+
const projectionUnsupported = skill.protected
|
|
659
|
+
|| skill.readonly === true
|
|
660
|
+
|| artifact?.state === 'unsupported'
|
|
661
|
+
|| projectionCapability.mode !== 'native-copy';
|
|
662
|
+
return {
|
|
663
|
+
kind: 'skill',
|
|
664
|
+
controlKey,
|
|
665
|
+
platform: this.platform,
|
|
666
|
+
scope: sourceScope,
|
|
667
|
+
projectPath,
|
|
668
|
+
sourceKey,
|
|
669
|
+
source: {
|
|
670
|
+
kind: skill.sourceProvider || skill.source || 'native',
|
|
671
|
+
repoId: skill.repoId || null,
|
|
672
|
+
fullDirectory: skill.fullDirectory || skill.directory || '',
|
|
673
|
+
revision: skill.revision || null
|
|
674
|
+
},
|
|
675
|
+
artifact: {
|
|
676
|
+
...(artifact || {}),
|
|
677
|
+
root: artifactRoot,
|
|
678
|
+
contentHash: skill.contentHash || artifact?.contentHash || null,
|
|
679
|
+
state: skill.artifactState || artifact?.state || (isRemote ? 'metadata_only' : 'ready'),
|
|
680
|
+
fetchedAt: artifact?.fetchedAt || null
|
|
681
|
+
},
|
|
682
|
+
targetDirectory: skill.directory,
|
|
683
|
+
cached: skill.cached !== false && skill.artifactState !== 'metadata_only',
|
|
684
|
+
enabled: isRemote ? false : skill.installed !== false,
|
|
685
|
+
trust: isRemote ? 'pending' : 'approved',
|
|
686
|
+
projection: {
|
|
687
|
+
mode: projectionUnsupported ? 'unsupported' : projectionCapability.mode,
|
|
688
|
+
sourceKey: !projectionUnsupported
|
|
689
|
+
&& !isRemote
|
|
690
|
+
&& (
|
|
691
|
+
skill.sourceProvider === 'native'
|
|
692
|
+
|| skill.source === 'native-installed'
|
|
693
|
+
|| skill.source === 'project-installed'
|
|
694
|
+
)
|
|
695
|
+
? sourceKey
|
|
696
|
+
: null,
|
|
697
|
+
path: null,
|
|
698
|
+
updatedAt: null
|
|
699
|
+
},
|
|
700
|
+
managed: true,
|
|
701
|
+
lastError: null,
|
|
702
|
+
createdAt: Date.now(),
|
|
703
|
+
updatedAt: Date.now()
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
_applyControlState(skills, options = {}) {
|
|
708
|
+
if (!this.controlService) {
|
|
709
|
+
return skills.map(skill => ({
|
|
710
|
+
...skill,
|
|
711
|
+
managed: true,
|
|
712
|
+
cached: skill.cached !== false,
|
|
713
|
+
enabled: skill.enabled === true || skill.installed === true,
|
|
714
|
+
trust: skill.trust || (skill.source === 'remote' ? 'pending' : 'approved'),
|
|
715
|
+
installed: skill.enabled === true || skill.installed === true
|
|
716
|
+
}));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return skills.map(skill => {
|
|
720
|
+
const candidate = this._buildSkillControlEntry(skill, options);
|
|
721
|
+
const scopeOptions = { scope: candidate.scope, projectPath: candidate.projectPath };
|
|
722
|
+
let entry = this.controlService.getSkill(candidate.controlKey, scopeOptions);
|
|
723
|
+
if (!entry && (skill.sourceProvider === 'native' || skill.source === 'native-installed')) {
|
|
724
|
+
const effective = this.controlService.getEffectiveSnapshot?.({
|
|
725
|
+
platform: this.platform,
|
|
726
|
+
scope: candidate.scope,
|
|
727
|
+
projectPath: candidate.projectPath
|
|
728
|
+
});
|
|
729
|
+
entry = effective?.skills?.active?.find(item => (
|
|
730
|
+
item.scope === candidate.scope
|
|
731
|
+
&& item.projectPath === candidate.projectPath
|
|
732
|
+
&& item.targetDirectory === candidate.targetDirectory
|
|
733
|
+
&& item.sourceKey !== candidate.sourceKey
|
|
734
|
+
&& item.artifact?.root
|
|
735
|
+
)) || null;
|
|
736
|
+
}
|
|
737
|
+
if (!entry) {
|
|
738
|
+
entry = this.controlService.registerSkill(candidate);
|
|
739
|
+
} else if (
|
|
740
|
+
candidate.artifact?.root
|
|
741
|
+
&& candidate.artifact?.contentHash
|
|
742
|
+
&& candidate.artifact.contentHash !== entry.artifact?.contentHash
|
|
743
|
+
) {
|
|
744
|
+
if (entry.enabled || entry.projection?.state === 'enabled') {
|
|
745
|
+
try {
|
|
746
|
+
this.controlService.setSkillEnabled({
|
|
747
|
+
platform: this.platform,
|
|
748
|
+
controlKey: entry.controlKey,
|
|
749
|
+
scope: candidate.scope,
|
|
750
|
+
projectPath: candidate.projectPath,
|
|
751
|
+
enabled: false
|
|
752
|
+
});
|
|
753
|
+
} catch (error) {
|
|
754
|
+
candidate.lastError = error.message;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
entry = this.controlService.registerSkill({
|
|
758
|
+
...entry,
|
|
759
|
+
artifact: { ...(entry.artifact || {}), ...candidate.artifact },
|
|
760
|
+
cached: candidate.cached,
|
|
761
|
+
enabled: false,
|
|
762
|
+
trust: 'needs_review',
|
|
763
|
+
projection: { ...(entry.projection || {}), state: 'disabled', updatedAt: Date.now() },
|
|
764
|
+
lastError: candidate.lastError || null,
|
|
765
|
+
updatedAt: Date.now()
|
|
766
|
+
});
|
|
767
|
+
} else if (
|
|
768
|
+
(skill.sourceProvider === 'native' || skill.source === 'native-installed')
|
|
769
|
+
&& entry.sourceKey === candidate.sourceKey
|
|
770
|
+
&& !entry.projection?.sourceKey
|
|
771
|
+
&& candidate.projection?.sourceKey
|
|
772
|
+
) {
|
|
773
|
+
entry = this.controlService.registerSkill({
|
|
774
|
+
...entry,
|
|
775
|
+
projection: {
|
|
776
|
+
...(entry.projection || {}),
|
|
777
|
+
sourceKey: candidate.projection.sourceKey,
|
|
778
|
+
state: entry.enabled ? 'enabled' : (entry.projection?.state || 'disabled'),
|
|
779
|
+
updatedAt: Date.now()
|
|
780
|
+
},
|
|
781
|
+
updatedAt: Date.now()
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
const loadable = entry.trust === 'approved' && entry.artifact?.state === 'ready';
|
|
785
|
+
return {
|
|
786
|
+
...skill,
|
|
787
|
+
controlKey: entry.controlKey,
|
|
788
|
+
sourceKey: entry.sourceKey || candidate.sourceKey,
|
|
789
|
+
scope: entry.scope,
|
|
790
|
+
sourceScope: entry.scope,
|
|
791
|
+
projectPath: entry.projectPath,
|
|
792
|
+
managed: true,
|
|
793
|
+
cached: entry.cached,
|
|
794
|
+
enabled: loadable && entry.enabled === true,
|
|
795
|
+
trust: entry.trust,
|
|
796
|
+
artifact: entry.artifact,
|
|
797
|
+
installed: loadable && entry.enabled === true,
|
|
798
|
+
lastError: entry.lastError || null
|
|
799
|
+
};
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
_latestRefreshTask(options = {}) {
|
|
804
|
+
if (!PATHS.skillRefreshTasks || !fs.existsSync(PATHS.skillRefreshTasks)) return null;
|
|
805
|
+
try {
|
|
806
|
+
const persisted = JSON.parse(fs.readFileSync(PATHS.skillRefreshTasks, 'utf8'));
|
|
807
|
+
const scope = options.scope || 'user';
|
|
808
|
+
const projectPath = scope === 'project' ? this._skillCwdKey(options) : null;
|
|
809
|
+
return (Array.isArray(persisted?.tasks) ? persisted.tasks : [])
|
|
810
|
+
.filter(task => (
|
|
811
|
+
task?.platform === this.platform
|
|
812
|
+
&& (task.scope || 'user') === scope
|
|
813
|
+
&& (task.projectPath || null) === projectPath
|
|
814
|
+
))
|
|
815
|
+
.sort((a, b) => Number(b.finishedAt || b.createdAt || 0) - Number(a.finishedAt || a.createdAt || 0))[0] || null;
|
|
816
|
+
} catch {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
_refreshSnapshot(options = {}) {
|
|
822
|
+
const scope = options.scope || 'user';
|
|
823
|
+
const projectPath = scope === 'project' ? this._skillCwdKey(options) : null;
|
|
824
|
+
const cached = this._readRawRemoteCache();
|
|
825
|
+
const matchesScope = item => {
|
|
826
|
+
const itemScope = item?.sourceScope || 'user';
|
|
827
|
+
if (itemScope === 'project') {
|
|
828
|
+
return scope === 'project' && item.projectPath === projectPath;
|
|
829
|
+
}
|
|
830
|
+
return !item.projectPath || item.projectPath === projectPath;
|
|
831
|
+
};
|
|
832
|
+
const cachedSkills = Array.isArray(cached?.skills)
|
|
833
|
+
? cached.skills.filter(matchesScope)
|
|
834
|
+
: [];
|
|
835
|
+
const cachedFetchedAt = cachedSkills.reduce(
|
|
836
|
+
(latest, skill) => Math.max(latest, Number(skill.fetchedAt || 0)),
|
|
837
|
+
0
|
|
838
|
+
) || (cachedSkills.length > 0 ? Number(cached?.fetchedAt || 0) : 0);
|
|
839
|
+
const artifactFetchedAt = (this.artifactStore?.list?.({ platform: this.platform }) || [])
|
|
840
|
+
.filter(matchesScope)
|
|
841
|
+
.filter(artifact => artifact.sourceProvider === 'remote' || artifact.source === 'remote' || artifact.repoId)
|
|
842
|
+
.reduce((latest, artifact) => Math.max(latest, Number(artifact.fetchedAt || 0)), 0);
|
|
843
|
+
const fetchedAt = Math.max(cachedFetchedAt, artifactFetchedAt);
|
|
844
|
+
const task = this._latestRefreshTask(options);
|
|
845
|
+
return {
|
|
846
|
+
state: task?.status || (fetchedAt > 0 ? 'idle' : 'never_fetched'),
|
|
847
|
+
taskId: task?.id || null,
|
|
848
|
+
fetchedAt: fetchedAt || null,
|
|
849
|
+
error: task?.error ? sanitizeRefreshError(task.error) : null
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
_ensureLegacyControlMigration() {
|
|
854
|
+
if (this._legacyMigrationChecked || !this.controlService || !PATHS.configRegistry || !PATHS.configs) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
this._legacyMigrationChecked = true;
|
|
858
|
+
try {
|
|
859
|
+
const { ConfigRegistryService } = require('./config-registry-service');
|
|
860
|
+
new ConfigRegistryService({ controlService: this.controlService }).migrateSkillControls();
|
|
861
|
+
} catch (error) {
|
|
862
|
+
this._legacyMigrationChecked = false;
|
|
863
|
+
console.warn('[SkillService] Legacy control migration skipped:', error.message);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
_controlOnlySkills(options = {}, knownControlKeys = new Set()) {
|
|
868
|
+
if (!this.controlService?.getEffectiveSnapshot) return [];
|
|
869
|
+
const scope = options.scope || 'user';
|
|
870
|
+
const projectPath = scope === 'project' ? this._skillCwdKey(options) : null;
|
|
871
|
+
const snapshot = this.controlService.getEffectiveSnapshot({
|
|
872
|
+
platform: this.platform,
|
|
873
|
+
scope,
|
|
874
|
+
projectPath
|
|
875
|
+
});
|
|
876
|
+
return (snapshot.skills?.active || [])
|
|
877
|
+
.filter(entry => entry?.controlKey && !knownControlKeys.has(entry.controlKey))
|
|
878
|
+
.map(entry => {
|
|
879
|
+
const artifactRoot = typeof entry.artifact?.root === 'string' ? entry.artifact.root : null;
|
|
880
|
+
const skillFile = artifactRoot ? path.join(artifactRoot, 'SKILL.md') : null;
|
|
881
|
+
const artifactAllowed = !artifactRoot || !this.artifactStore?.root || (() => {
|
|
882
|
+
try {
|
|
883
|
+
assertNoSymlinkComponents(this.artifactStore.root, artifactRoot, fs);
|
|
884
|
+
const relative = path.relative(path.resolve(this.artifactStore.root), path.resolve(artifactRoot));
|
|
885
|
+
return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
886
|
+
} catch {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
})();
|
|
890
|
+
const available = Boolean(
|
|
891
|
+
artifactAllowed
|
|
892
|
+
&& skillFile
|
|
893
|
+
&& path.isAbsolute(artifactRoot)
|
|
894
|
+
&& fs.existsSync(skillFile)
|
|
895
|
+
&& !fs.lstatSync(artifactRoot).isSymbolicLink()
|
|
896
|
+
&& !fs.lstatSync(skillFile).isSymbolicLink()
|
|
897
|
+
);
|
|
898
|
+
const artifactState = available ? (entry.artifact?.state || 'ready') : 'missing';
|
|
899
|
+
const loadable = available && artifactState === 'ready' && entry.trust === 'approved';
|
|
900
|
+
const source = entry.source || {};
|
|
901
|
+
const sourceKind = typeof source === 'string' ? source : source.kind || 'unknown';
|
|
902
|
+
const directory = String(entry.targetDirectory || source.fullDirectory || entry.name || entry.controlKey);
|
|
903
|
+
return {
|
|
904
|
+
key: entry.controlKey,
|
|
905
|
+
controlKey: entry.controlKey,
|
|
906
|
+
sourceKey: entry.sourceKey || null,
|
|
907
|
+
name: entry.name || path.basename(directory),
|
|
908
|
+
description: entry.description || '',
|
|
909
|
+
directory,
|
|
910
|
+
installed: loadable && entry.enabled === true,
|
|
911
|
+
enabled: loadable && entry.enabled === true,
|
|
912
|
+
cached: available && entry.cached !== false,
|
|
913
|
+
isLocal: sourceKind === 'local' || sourceKind === 'template',
|
|
914
|
+
source: sourceKind === 'remote' || sourceKind === 'git' ? 'remote' : sourceKind,
|
|
915
|
+
sourceProvider: sourceKind,
|
|
916
|
+
sourceScope: entry.scope,
|
|
917
|
+
scope: entry.scope,
|
|
918
|
+
projectPath: entry.projectPath || null,
|
|
919
|
+
sourcePath: skillFile,
|
|
920
|
+
artifact: {
|
|
921
|
+
...(entry.artifact || {}),
|
|
922
|
+
root: artifactRoot,
|
|
923
|
+
state: artifactState
|
|
924
|
+
},
|
|
925
|
+
artifactRoot,
|
|
926
|
+
projectionOverlay: true,
|
|
927
|
+
artifactState,
|
|
928
|
+
contentHash: entry.artifact?.contentHash || null,
|
|
929
|
+
revision: source.revision || null,
|
|
930
|
+
repoId: source.repoId || null,
|
|
931
|
+
cachedAt: entry.artifact?.fetchedAt || null,
|
|
932
|
+
trust: entry.trust || 'pending',
|
|
933
|
+
managed: true,
|
|
934
|
+
projection: entry.projection || { mode: 'unsupported', state: 'unsupported' },
|
|
935
|
+
lastError: entry.lastError || (!available ? 'Skill artifact is missing' : null),
|
|
936
|
+
readonly: sourceKind === 'remote' || sourceKind === 'git',
|
|
937
|
+
protected: false,
|
|
938
|
+
shadowedSources: [],
|
|
939
|
+
readmeUrl: null,
|
|
940
|
+
license: null
|
|
941
|
+
};
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async scanSkills(options = {}) {
|
|
946
|
+
this._ensureLegacyControlMigration();
|
|
947
|
+
const scope = options.scope || 'user';
|
|
948
|
+
const scopeOptions = scope === 'project'
|
|
949
|
+
? this.resolveScopeOptions(options)
|
|
950
|
+
: { scope: 'user', cwd: '' };
|
|
951
|
+
const normalizedOptions = {
|
|
952
|
+
...options,
|
|
953
|
+
scope,
|
|
954
|
+
...(scope === 'project' ? { cwd: scopeOptions.cwd } : {})
|
|
955
|
+
};
|
|
956
|
+
const cacheKey = this._skillCacheKey({
|
|
957
|
+
scope,
|
|
958
|
+
cwd: options.cwd || (scope === 'project' ? scopeOptions.cwd : '')
|
|
959
|
+
});
|
|
960
|
+
const generation = this._cacheGeneration;
|
|
961
|
+
const rawSkills = [
|
|
962
|
+
...this._artifactSkills(normalizedOptions),
|
|
963
|
+
...(options.includeRemote === true ? this._legacyCachedSkills(normalizedOptions) : [])
|
|
964
|
+
];
|
|
965
|
+
const prepared = this.prepareSkills(rawSkills, normalizedOptions);
|
|
966
|
+
const controlled = this._applyControlState(prepared, normalizedOptions);
|
|
967
|
+
const knownControlKeys = new Set(controlled.map(skill => skill.controlKey).filter(Boolean));
|
|
968
|
+
const missingControls = this._controlOnlySkills(normalizedOptions, knownControlKeys);
|
|
969
|
+
const skills = [...controlled, ...missingControls];
|
|
970
|
+
this.deduplicateSkills(skills);
|
|
971
|
+
const preparedSkills = this._storePrepared(cacheKey, skills, generation);
|
|
972
|
+
return {
|
|
973
|
+
skills: preparedSkills,
|
|
974
|
+
refresh: this._refreshSnapshot(normalizedOptions)
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
_readRawRemoteCache() {
|
|
979
|
+
const now = Date.now();
|
|
980
|
+
if (Array.isArray(this._remoteSkillsCache)) {
|
|
981
|
+
return {
|
|
982
|
+
skills: this._remoteSkillsCache,
|
|
983
|
+
fetchedAt: this._remoteSkillsFetchedAt,
|
|
984
|
+
fresh: this._remoteSkillsFetchedAt > 0 && now - this._remoteSkillsFetchedAt < CACHE_TTL
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const cached = this._readCacheEnvelope();
|
|
989
|
+
if (!cached) return null;
|
|
990
|
+
this._remoteSkillsCache = cached.skills;
|
|
991
|
+
this._remoteSkillsFetchedAt = cached.fetchedAt;
|
|
992
|
+
return {
|
|
993
|
+
skills: cached.skills,
|
|
994
|
+
fetchedAt: cached.fetchedAt,
|
|
995
|
+
fresh: cached.fetchedAt > 0 && now - cached.fetchedAt < CACHE_TTL
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
|
|
291
1000
|
prepareSkills(skills = [], options = {}) {
|
|
292
1001
|
this.refreshOmpPaths();
|
|
293
1002
|
const preparedSkills = Array.isArray(skills)
|
|
@@ -295,15 +1004,21 @@ class SkillService {
|
|
|
295
1004
|
: [];
|
|
296
1005
|
|
|
297
1006
|
if (this.platform === 'omp') {
|
|
298
|
-
preparedSkills.unshift(...discoverOmpSkills(this, options));
|
|
1007
|
+
preparedSkills.unshift(...discoverOmpSkills(this, { ...options, pathContext: this.pathContext }));
|
|
299
1008
|
}
|
|
300
1009
|
this.mergeLocalSkills(preparedSkills);
|
|
301
1010
|
if (this.platform !== 'omp') {
|
|
302
|
-
this.mergeInstalledSkills(preparedSkills);
|
|
1011
|
+
this.mergeInstalledSkills(preparedSkills, options);
|
|
1012
|
+
if (options.scope === 'project') {
|
|
1013
|
+
this.mergeProjectSkills(preparedSkills, options);
|
|
1014
|
+
}
|
|
303
1015
|
}
|
|
304
1016
|
this.deduplicateSkills(preparedSkills);
|
|
305
1017
|
preparedSkills.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
306
|
-
this.updateInstallStatus(preparedSkills
|
|
1018
|
+
this.updateInstallStatus(preparedSkills, {
|
|
1019
|
+
pathsRefreshed: this.platform === 'omp',
|
|
1020
|
+
options
|
|
1021
|
+
});
|
|
307
1022
|
|
|
308
1023
|
return preparedSkills;
|
|
309
1024
|
}
|
|
@@ -360,7 +1075,7 @@ class SkillService {
|
|
|
360
1075
|
} else if (provider === 'gitlab') {
|
|
361
1076
|
normalized.host = normalizeRepoHost(repo.host, 'gitlab');
|
|
362
1077
|
normalized.projectPath = normalizeRepoPath(repo.projectPath || [repo.owner, repo.name].filter(Boolean).join('/'));
|
|
363
|
-
if (!normalized.projectPath) {
|
|
1078
|
+
if (!normalized.projectPath || normalized.projectPath.split('/').some(segment => !segment || segment === '.' || segment === '..')) {
|
|
364
1079
|
throw new Error('Missing GitLab project path');
|
|
365
1080
|
}
|
|
366
1081
|
normalized.name = stripGitSuffix(normalized.projectPath.split('/').pop() || '');
|
|
@@ -374,7 +1089,7 @@ class SkillService {
|
|
|
374
1089
|
}
|
|
375
1090
|
}
|
|
376
1091
|
|
|
377
|
-
normalized.repoUrl = repo.repoUrl
|
|
1092
|
+
normalized.repoUrl = sanitizeRepoUrl(repo.repoUrl, buildRepoUrl(normalized));
|
|
378
1093
|
normalized.label = buildRepoLabel(normalized);
|
|
379
1094
|
normalized.id = buildRepoId(normalized);
|
|
380
1095
|
|
|
@@ -500,16 +1215,87 @@ class SkillService {
|
|
|
500
1215
|
removeRepo(owner, name, directory = '', repoId = '') {
|
|
501
1216
|
const repos = this.loadRepos();
|
|
502
1217
|
const normalizedDirectory = normalizeRepoDirectory(directory);
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
1218
|
+
const removedRepoIds = new Set(
|
|
1219
|
+
repos
|
|
1220
|
+
.filter(r => {
|
|
1221
|
+
if (repoId) {
|
|
1222
|
+
return r.id === repoId;
|
|
1223
|
+
}
|
|
1224
|
+
return (
|
|
1225
|
+
(r.owner || '') === owner &&
|
|
1226
|
+
(r.name || '') === name &&
|
|
1227
|
+
normalizeRepoDirectory(r.directory) === normalizedDirectory
|
|
1228
|
+
);
|
|
1229
|
+
})
|
|
1230
|
+
.map(r => r.id)
|
|
1231
|
+
.filter(Boolean)
|
|
1232
|
+
);
|
|
1233
|
+
const filtered = repos.filter(r => !removedRepoIds.has(r.id) && !(
|
|
1234
|
+
!repoId &&
|
|
1235
|
+
(r.owner || '') === owner &&
|
|
1236
|
+
(r.name || '') === name &&
|
|
1237
|
+
normalizeRepoDirectory(r.directory) === normalizedDirectory
|
|
1238
|
+
));
|
|
1239
|
+
if (this.artifactStore && removedRepoIds.size > 0) {
|
|
1240
|
+
const disabledControlKeys = new Set();
|
|
1241
|
+
for (const artifact of this.artifactStore.list({ platform: this.platform })) {
|
|
1242
|
+
if (!removedRepoIds.has(artifact.repoId)) continue;
|
|
1243
|
+
const scope = artifact.sourceScope || 'user';
|
|
1244
|
+
const projectPath = scope === 'project' ? artifact.projectPath : null;
|
|
1245
|
+
const controlKey = `skill:${this.platform}:${scope}:${projectPath || 'user'}:${artifact.sourceKey}`;
|
|
1246
|
+
if (!disabledControlKeys.has(controlKey) && this.controlService?.getSkill && this.controlService?.setSkillEnabled) {
|
|
1247
|
+
let existing = null;
|
|
1248
|
+
try {
|
|
1249
|
+
existing = this.controlService.getSkill(controlKey, { scope, projectPath });
|
|
1250
|
+
} catch (_) {
|
|
1251
|
+
// A deleted project has no readable project control manifest.
|
|
1252
|
+
}
|
|
1253
|
+
if (existing) {
|
|
1254
|
+
try {
|
|
1255
|
+
const disabled = this.controlService.setSkillEnabled({
|
|
1256
|
+
platform: this.platform,
|
|
1257
|
+
controlKey,
|
|
1258
|
+
scope,
|
|
1259
|
+
projectPath,
|
|
1260
|
+
enabled: false
|
|
1261
|
+
});
|
|
1262
|
+
if (disabled?.enabled !== false && this.controlService.registerSkill) {
|
|
1263
|
+
this.controlService.registerSkill({
|
|
1264
|
+
...existing,
|
|
1265
|
+
enabled: false,
|
|
1266
|
+
projection: {
|
|
1267
|
+
...(existing.projection || {}),
|
|
1268
|
+
...(disabled?.projection || {}),
|
|
1269
|
+
state: disabled?.projection?.state || 'conflict',
|
|
1270
|
+
status: disabled?.projection?.status || 'conflict',
|
|
1271
|
+
updatedAt: Date.now()
|
|
1272
|
+
},
|
|
1273
|
+
lastError: disabled?.lastError || 'Skill projection ownership could not be verified',
|
|
1274
|
+
updatedAt: Date.now()
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
if (this.controlService.registerSkill) {
|
|
1279
|
+
this.controlService.registerSkill({
|
|
1280
|
+
...existing,
|
|
1281
|
+
enabled: false,
|
|
1282
|
+
projection: { ...(existing.projection || {}), state: 'error', status: 'error', updatedAt: Date.now() },
|
|
1283
|
+
lastError: error.message,
|
|
1284
|
+
updatedAt: Date.now()
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
disabledControlKeys.add(controlKey);
|
|
1290
|
+
}
|
|
1291
|
+
this.artifactStore.markState({
|
|
1292
|
+
platform: this.platform,
|
|
1293
|
+
sourceKey: artifact.sourceKey,
|
|
1294
|
+
format: artifact.format,
|
|
1295
|
+
state: 'orphaned'
|
|
1296
|
+
});
|
|
506
1297
|
}
|
|
507
|
-
|
|
508
|
-
(r.owner || '') === owner &&
|
|
509
|
-
(r.name || '') === name &&
|
|
510
|
-
normalizeRepoDirectory(r.directory) === normalizedDirectory
|
|
511
|
-
);
|
|
512
|
-
});
|
|
1298
|
+
}
|
|
513
1299
|
this.saveRepos(filtered);
|
|
514
1300
|
this.clearCache({ removeFile: true });
|
|
515
1301
|
return this.loadRepos();
|
|
@@ -581,159 +1367,86 @@ class SkillService {
|
|
|
581
1367
|
}
|
|
582
1368
|
|
|
583
1369
|
/**
|
|
584
|
-
*
|
|
1370
|
+
* 获取本地 Skill 列表。网络刷新必须由显式 refresh task 触发。
|
|
1371
|
+
*
|
|
1372
|
+
* @deprecated 内部调用请使用 scanSkills;此方法只保留现有脚本的数组响应兼容性。
|
|
585
1373
|
*/
|
|
586
|
-
async listSkills(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const fileCache = this.loadCacheFromFile();
|
|
596
|
-
|
|
597
|
-
// 检查内存缓存
|
|
598
|
-
if (!forceRefresh && Array.isArray(this.skillsCache) && this.skillsCache.length > 0) {
|
|
599
|
-
if (Array.isArray(fileCache) && fileCache.length > this.skillsCache.length) {
|
|
600
|
-
this.skillsCache = this.prepareSkills(fileCache);
|
|
601
|
-
this.cacheTime = Date.now();
|
|
602
|
-
return this.skillsCache;
|
|
603
|
-
}
|
|
604
|
-
this.skillsCache = this.prepareSkills(this.skillsCache);
|
|
605
|
-
this.cacheTime = Date.now();
|
|
606
|
-
return this.skillsCache;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// 检查文件缓存
|
|
610
|
-
if (!forceRefresh) {
|
|
611
|
-
if (fileCache && fileCache.length > 0) {
|
|
612
|
-
this.skillsCache = this.prepareSkills(fileCache);
|
|
613
|
-
this.cacheTime = Date.now();
|
|
614
|
-
return this.skillsCache;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const repos = this.loadRepos();
|
|
619
|
-
const skills = [];
|
|
620
|
-
|
|
621
|
-
// 并行获取所有启用仓库的技能(带超时保护)
|
|
622
|
-
const enabledRepos = repos.filter(r => r.enabled);
|
|
623
|
-
const enabledRemoteRepos = enabledRepos.filter(repo => repo.provider !== 'local');
|
|
624
|
-
let remoteFailureCount = 0;
|
|
625
|
-
|
|
626
|
-
if (enabledRepos.length > 0) {
|
|
627
|
-
const results = await Promise.allSettled(
|
|
628
|
-
enabledRepos.map(repo =>
|
|
629
|
-
Promise.race([
|
|
630
|
-
this.fetchRepoSkills(repo),
|
|
631
|
-
new Promise((_, reject) =>
|
|
632
|
-
setTimeout(() => reject(new Error('Fetch timeout')), 30000) // 30秒超时
|
|
633
|
-
)
|
|
634
|
-
])
|
|
635
|
-
)
|
|
636
|
-
);
|
|
637
|
-
|
|
638
|
-
for (let i = 0; i < results.length; i++) {
|
|
639
|
-
const result = results[i];
|
|
640
|
-
const repo = enabledRepos[i];
|
|
641
|
-
const repoInfo = `${repo.owner}/${repo.name}`;
|
|
642
|
-
if (result.status === 'fulfilled') {
|
|
643
|
-
skills.push(...result.value);
|
|
644
|
-
} else {
|
|
645
|
-
console.warn(`[SkillService] Fetch repo ${repoInfo} failed:`, result.reason?.message);
|
|
646
|
-
if (repo.provider !== 'local') {
|
|
647
|
-
remoteFailureCount++;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
1374
|
+
async listSkills(options = {}, legacyOptions = {}) {
|
|
1375
|
+
const scanOptions = typeof options === 'boolean'
|
|
1376
|
+
? {
|
|
1377
|
+
...legacyOptions,
|
|
1378
|
+
...(options ? { force: true } : {}),
|
|
1379
|
+
scope: legacyOptions.scope || (legacyOptions.cwd ? 'project' : 'user'),
|
|
1380
|
+
includeRemote: legacyOptions.includeRemote !== false
|
|
650
1381
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const hasUsableFileCache = Array.isArray(fileCache) && fileCache.length > 0;
|
|
656
|
-
const preparedFileCache = hasUsableFileCache ? this.prepareSkills(fileCache) : null;
|
|
657
|
-
const shouldUseStaleFileCache = hasUsableFileCache && (
|
|
658
|
-
(enabledRemoteRepos.length > 0 && remoteFailureCount === enabledRemoteRepos.length) ||
|
|
659
|
-
(remoteFailureCount > 0 && preparedFileCache.length > preparedSkills.length)
|
|
660
|
-
);
|
|
661
|
-
|
|
662
|
-
if (shouldUseStaleFileCache) {
|
|
663
|
-
this.skillsCache = preparedFileCache;
|
|
664
|
-
this.cacheTime = Date.now();
|
|
665
|
-
return this.skillsCache;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// 更新缓存
|
|
669
|
-
this.skillsCache = preparedSkills;
|
|
670
|
-
this.cacheTime = Date.now();
|
|
671
|
-
this.saveCacheToFile(preparedSkills);
|
|
672
|
-
|
|
673
|
-
return preparedSkills;
|
|
1382
|
+
: { ...(options || {}), includeRemote: options?.includeRemote !== false };
|
|
1383
|
+
const result = await this.scanSkills(scanOptions || {});
|
|
1384
|
+
return result.skills;
|
|
674
1385
|
}
|
|
675
1386
|
|
|
676
|
-
async listOmpSkills(options = {}) {
|
|
677
|
-
this.refreshOmpPaths();
|
|
678
|
-
const skills = [];
|
|
679
|
-
const repos = this.loadRepos().filter(repo => repo.enabled);
|
|
680
|
-
const results = await Promise.allSettled(repos.map(repo => this.fetchRepoSkills(repo)));
|
|
681
|
-
results.forEach((result, index) => {
|
|
682
|
-
if (result.status === 'fulfilled') {
|
|
683
|
-
skills.push(...result.value);
|
|
684
|
-
} else {
|
|
685
|
-
console.warn(
|
|
686
|
-
`[SkillService] Fetch OMP repo ${repos[index]?.label || repos[index]?.id || ''} failed:`,
|
|
687
|
-
result.reason?.message
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
|
-
const prepared = this.prepareSkills(skills, options);
|
|
692
|
-
this.skillsCache = prepared;
|
|
693
|
-
this.cacheTime = Date.now();
|
|
694
|
-
return prepared;
|
|
695
|
-
}
|
|
696
1387
|
|
|
697
1388
|
/**
|
|
698
1389
|
* 从文件加载缓存
|
|
699
1390
|
*/
|
|
700
|
-
|
|
1391
|
+
_readCacheEnvelope() {
|
|
701
1392
|
try {
|
|
702
|
-
if (fs.existsSync(this.cachePath))
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1393
|
+
if (!fs.existsSync(this.cachePath)) return null;
|
|
1394
|
+
const data = JSON.parse(fs.readFileSync(this.cachePath, 'utf-8'));
|
|
1395
|
+
if (Array.isArray(data)) {
|
|
1396
|
+
return { skills: data, fetchedAt: 0 };
|
|
1397
|
+
}
|
|
1398
|
+
if (Array.isArray(data?.skills)) {
|
|
1399
|
+
return {
|
|
1400
|
+
skills: data.skills,
|
|
1401
|
+
fetchedAt: Number(data.fetchedAt || data.time || 0) || 0
|
|
1402
|
+
};
|
|
707
1403
|
}
|
|
708
1404
|
} catch (err) {
|
|
709
|
-
//
|
|
1405
|
+
// Ignore malformed cache files and refresh from repositories.
|
|
710
1406
|
}
|
|
711
1407
|
return null;
|
|
712
1408
|
}
|
|
713
1409
|
|
|
1410
|
+
/**
|
|
1411
|
+
* 从文件加载缓存
|
|
1412
|
+
*/
|
|
1413
|
+
loadCacheFromFile() {
|
|
1414
|
+
return this._readCacheEnvelope()?.skills || null;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
714
1417
|
/**
|
|
715
1418
|
* 保存缓存到文件
|
|
716
1419
|
*/
|
|
717
|
-
saveCacheToFile(skills) {
|
|
1420
|
+
saveCacheToFile(skills, fetchedAt = Date.now()) {
|
|
718
1421
|
try {
|
|
719
1422
|
fs.writeFileSync(this.cachePath, JSON.stringify({
|
|
720
|
-
|
|
1423
|
+
fetchedAt,
|
|
1424
|
+
time: fetchedAt,
|
|
721
1425
|
skills
|
|
722
1426
|
}));
|
|
723
1427
|
} catch (err) {
|
|
724
|
-
//
|
|
1428
|
+
// Ignore cache write failures; callers retain the in-memory value.
|
|
725
1429
|
}
|
|
726
1430
|
}
|
|
727
1431
|
|
|
728
1432
|
/**
|
|
729
1433
|
* 更新技能的安装状态
|
|
730
1434
|
*/
|
|
731
|
-
updateInstallStatus(skills) {
|
|
1435
|
+
updateInstallStatus(skills, { pathsRefreshed = false, options = {} } = {}) {
|
|
1436
|
+
if (this.platform === 'omp' && !pathsRefreshed) {
|
|
1437
|
+
this.refreshOmpPaths();
|
|
1438
|
+
}
|
|
1439
|
+
|
|
732
1440
|
for (const skill of skills) {
|
|
733
|
-
if (skill.
|
|
1441
|
+
if (skill.sourceScope === 'project') {
|
|
1442
|
+
skill.installed = true;
|
|
1443
|
+
} else if (skill.sourceProvider || skill.sourcePath || skill.readonly) {
|
|
734
1444
|
skill.installed = skill.installed !== false;
|
|
735
1445
|
} else {
|
|
736
|
-
|
|
1446
|
+
const installOptions = options.scope === 'project'
|
|
1447
|
+
? { scope: 'project', cwd: options.cwd, refresh: false }
|
|
1448
|
+
: { scope: 'user', refresh: false };
|
|
1449
|
+
skill.installed = this.isInstalled(skill.directory, installOptions);
|
|
737
1450
|
}
|
|
738
1451
|
}
|
|
739
1452
|
}
|
|
@@ -754,6 +1467,592 @@ class SkillService {
|
|
|
754
1467
|
return this.fetchGitHubRepoSkills(repo);
|
|
755
1468
|
}
|
|
756
1469
|
|
|
1470
|
+
_skillBundleSourceKey(repo, fullDirectory) {
|
|
1471
|
+
return `repo:${repo.id || buildRepoId(repo)}:${normalizeRepoPath(fullDirectory)}`;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
_treeSkillRoots(treeItems, repo) {
|
|
1475
|
+
const baseDir = normalizeRepoDirectory(repo.directory);
|
|
1476
|
+
if (baseDir) {
|
|
1477
|
+
normalizeSkillRelativePath(baseDir, 'skill repository directory', { allowHiddenSegments: true });
|
|
1478
|
+
}
|
|
1479
|
+
const basePrefix = baseDir ? `${baseDir}/` : '';
|
|
1480
|
+
return treeItems
|
|
1481
|
+
.filter(item => {
|
|
1482
|
+
if (!item || item.type !== 'blob' || !isRootSkillFile(item.path)) return false;
|
|
1483
|
+
const normalizedPath = normalizeRepoPath(item.path);
|
|
1484
|
+
try {
|
|
1485
|
+
normalizeSkillRelativePath(normalizedPath, 'skill repository file path', { allowHiddenSegments: true });
|
|
1486
|
+
} catch {
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
return !baseDir || normalizedPath === baseDir || normalizedPath.startsWith(basePrefix);
|
|
1490
|
+
})
|
|
1491
|
+
.map(item => ({
|
|
1492
|
+
file: item,
|
|
1493
|
+
fullDirectory: normalizeRepoPath(item.path).replace(/\/SKILL\.md$/i, '')
|
|
1494
|
+
}))
|
|
1495
|
+
.filter(item => item.fullDirectory)
|
|
1496
|
+
.sort((a, b) => a.fullDirectory.length - b.fullDirectory.length);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
async _fetchGitSkillBundles(repo, treeItems, revision = null) {
|
|
1500
|
+
const roots = this._treeSkillRoots(treeItems, repo);
|
|
1501
|
+
const rootPaths = roots.map(item => item.fullDirectory);
|
|
1502
|
+
const bundles = [];
|
|
1503
|
+
|
|
1504
|
+
for (const root of roots) {
|
|
1505
|
+
const prefix = `${root.fullDirectory}/`;
|
|
1506
|
+
const nestedRoots = rootPaths.filter(candidate => (
|
|
1507
|
+
candidate !== root.fullDirectory && candidate.startsWith(prefix)
|
|
1508
|
+
));
|
|
1509
|
+
const files = treeItems
|
|
1510
|
+
.filter(item => {
|
|
1511
|
+
if (!item || item.type !== 'blob') return false;
|
|
1512
|
+
const filePath = normalizeRepoPath(item.path);
|
|
1513
|
+
if (filePath !== `${root.fullDirectory}/SKILL.md` && !filePath.startsWith(prefix)) return false;
|
|
1514
|
+
normalizeSkillRelativePath(filePath, 'skill repository file path', { allowHiddenSegments: true });
|
|
1515
|
+
if (item.mode === '120000' || item.type === 'symlink') {
|
|
1516
|
+
throw new Error(`Skill repository contains symlink: ${filePath}`);
|
|
1517
|
+
}
|
|
1518
|
+
return !nestedRoots.some(nested => filePath === nested || filePath.startsWith(`${nested}/`));
|
|
1519
|
+
})
|
|
1520
|
+
.map(item => ({
|
|
1521
|
+
relativePath: normalizeRepoPath(item.path).slice(prefix.length),
|
|
1522
|
+
source: item
|
|
1523
|
+
}));
|
|
1524
|
+
const contentFiles = [];
|
|
1525
|
+
for (const file of files) {
|
|
1526
|
+
const content = await this.fetchSkillFileContent(repo, file.source);
|
|
1527
|
+
contentFiles.push({ relativePath: file.relativePath, content });
|
|
1528
|
+
}
|
|
1529
|
+
const skillMd = contentFiles.find(file => file.relativePath === 'SKILL.md');
|
|
1530
|
+
if (!skillMd) continue;
|
|
1531
|
+
const metadata = this.parseSkillMd(String(skillMd.content));
|
|
1532
|
+
const directory = this.resolveSkillDirectory(root.fullDirectory, repo.directory || '', repo);
|
|
1533
|
+
bundles.push({
|
|
1534
|
+
sourceKey: this._skillBundleSourceKey(repo, root.fullDirectory),
|
|
1535
|
+
directory,
|
|
1536
|
+
fullDirectory: root.fullDirectory,
|
|
1537
|
+
files: contentFiles,
|
|
1538
|
+
metadata: {
|
|
1539
|
+
name: metadata.name || directory,
|
|
1540
|
+
description: metadata.description || '',
|
|
1541
|
+
revision: revision || repo.revision || repo.commit || null,
|
|
1542
|
+
repoId: repo.id,
|
|
1543
|
+
repoOwner: repo.owner || null,
|
|
1544
|
+
repoName: repo.name || null,
|
|
1545
|
+
repoBranch: repo.branch || null,
|
|
1546
|
+
repoProvider: repo.provider,
|
|
1547
|
+
repoUrl: repo.repoUrl || buildRepoUrl(repo),
|
|
1548
|
+
readmeUrl: this.buildSkillReadmeUrl(repo, root.fullDirectory)
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
return bundles;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
_collectLocalSkillFiles(skillDir) {
|
|
1556
|
+
const files = [];
|
|
1557
|
+
const visit = currentDir => {
|
|
1558
|
+
let entries;
|
|
1559
|
+
try {
|
|
1560
|
+
if (fs.lstatSync(currentDir).isSymbolicLink()) {
|
|
1561
|
+
throw new Error(`Skill repository contains symlink: ${currentDir}`);
|
|
1562
|
+
}
|
|
1563
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
if (error.message.includes('symlink')) throw error;
|
|
1566
|
+
throw new Error(`Unable to read local Skill directory: ${currentDir}`, { cause: error });
|
|
1567
|
+
}
|
|
1568
|
+
for (const entry of entries) {
|
|
1569
|
+
const sourcePath = path.join(currentDir, entry.name);
|
|
1570
|
+
if (entry.isSymbolicLink()) {
|
|
1571
|
+
throw new Error(`Skill repository contains symlink: ${sourcePath}`);
|
|
1572
|
+
}
|
|
1573
|
+
if (entry.isDirectory()) {
|
|
1574
|
+
if (entry.name === 'node_modules') continue;
|
|
1575
|
+
if (currentDir !== skillDir && fs.existsSync(path.join(sourcePath, 'SKILL.md'))) {
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
visit(sourcePath);
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
if (!entry.isFile()) continue;
|
|
1582
|
+
const relativePath = normalizeSkillRelativePath(path.relative(skillDir, sourcePath), 'skill file path', {
|
|
1583
|
+
allowHiddenSegments: true
|
|
1584
|
+
});
|
|
1585
|
+
files.push({ relativePath, content: fs.readFileSync(sourcePath) });
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
visit(skillDir);
|
|
1589
|
+
return files;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
_registerCreatedSkill({ root, directory, scope = 'user', cwd = null }) {
|
|
1593
|
+
const files = this._collectLocalSkillFiles(root);
|
|
1594
|
+
const normalized = this.formatAdapter.normalize({
|
|
1595
|
+
platform: this.platform,
|
|
1596
|
+
files,
|
|
1597
|
+
sourceMetadata: { name: directory }
|
|
1598
|
+
});
|
|
1599
|
+
const projectPath = scope === 'project' ? this._skillCwdKey({ cwd }) : null;
|
|
1600
|
+
const sourceKey = projectPath
|
|
1601
|
+
? `local:${this.platform}:project:${projectPath}:${directory}`
|
|
1602
|
+
: `local:${this.platform}:${directory}`;
|
|
1603
|
+
const controlKey = `skill:${this.platform}:${scope}:${projectPath || 'user'}:${sourceKey}`;
|
|
1604
|
+
const projectionCapability = this._skillProjectionCapability(scope);
|
|
1605
|
+
const published = this.artifactStore
|
|
1606
|
+
? this.artifactStore.publishSkill({
|
|
1607
|
+
platform: this.platform,
|
|
1608
|
+
sourceKey,
|
|
1609
|
+
format: normalized.format,
|
|
1610
|
+
files: normalized.files,
|
|
1611
|
+
metadata: {
|
|
1612
|
+
name: directory,
|
|
1613
|
+
directory,
|
|
1614
|
+
sourceProvider: 'local',
|
|
1615
|
+
sourceScope: scope,
|
|
1616
|
+
...(projectPath ? { projectPath } : {}),
|
|
1617
|
+
revision: null
|
|
1618
|
+
}
|
|
1619
|
+
})
|
|
1620
|
+
: {
|
|
1621
|
+
root,
|
|
1622
|
+
state: 'ready',
|
|
1623
|
+
contentHash: null,
|
|
1624
|
+
fetchedAt: Date.now()
|
|
1625
|
+
};
|
|
1626
|
+
const entry = {
|
|
1627
|
+
kind: 'skill',
|
|
1628
|
+
controlKey,
|
|
1629
|
+
platform: this.platform,
|
|
1630
|
+
scope,
|
|
1631
|
+
projectPath,
|
|
1632
|
+
sourceKey,
|
|
1633
|
+
source: {
|
|
1634
|
+
kind: 'local',
|
|
1635
|
+
repoId: null,
|
|
1636
|
+
fullDirectory: directory,
|
|
1637
|
+
revision: null
|
|
1638
|
+
},
|
|
1639
|
+
artifact: {
|
|
1640
|
+
root: published.root,
|
|
1641
|
+
contentHash: published.contentHash || null,
|
|
1642
|
+
state: published.state || 'ready',
|
|
1643
|
+
fetchedAt: published.fetchedAt || Date.now()
|
|
1644
|
+
},
|
|
1645
|
+
targetDirectory: directory,
|
|
1646
|
+
cached: true,
|
|
1647
|
+
enabled: false,
|
|
1648
|
+
trust: 'pending',
|
|
1649
|
+
projection: {
|
|
1650
|
+
mode: projectionCapability.mode,
|
|
1651
|
+
state: projectionCapability.mode === 'native-copy' ? 'disabled' : 'unsupported',
|
|
1652
|
+
path: null,
|
|
1653
|
+
updatedAt: null
|
|
1654
|
+
},
|
|
1655
|
+
managed: true,
|
|
1656
|
+
lastError: null,
|
|
1657
|
+
createdAt: Date.now(),
|
|
1658
|
+
updatedAt: Date.now()
|
|
1659
|
+
};
|
|
1660
|
+
const registered = this.controlService?.registerSkill
|
|
1661
|
+
? this.controlService.registerSkill(entry)
|
|
1662
|
+
: entry;
|
|
1663
|
+
return {
|
|
1664
|
+
controlKey: registered.controlKey,
|
|
1665
|
+
artifact: registered.artifact,
|
|
1666
|
+
cached: true,
|
|
1667
|
+
enabled: false,
|
|
1668
|
+
trust: 'pending',
|
|
1669
|
+
managed: true,
|
|
1670
|
+
warnings: normalized.warnings
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
registerTemplateSkill({ directory, files = null, repo = null, fullDirectory = null, scope = 'user', cwd = null } = {}) {
|
|
1675
|
+
const safeDirectory = this.normalizeSkillDirectory(directory);
|
|
1676
|
+
const projectPath = scope === 'project' ? this._skillCwdKey({ cwd }) : null;
|
|
1677
|
+
const projectionCapability = this._skillProjectionCapability(scope);
|
|
1678
|
+
if (scope === 'project' && !projectPath) {
|
|
1679
|
+
throw new Error('Project template Skill requires a valid cwd');
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
if (repo) {
|
|
1683
|
+
const normalizedRepo = this.normalizeRepoConfig(repo);
|
|
1684
|
+
const sourceDirectory = fullDirectory || safeDirectory;
|
|
1685
|
+
const sourceKey = this._skillBundleSourceKey(normalizedRepo, sourceDirectory);
|
|
1686
|
+
const controlKey = `skill:${this.platform}:user:user:${sourceKey}`;
|
|
1687
|
+
const existing = this.controlService?.getSkill?.(controlKey, { scope: 'user' }) || null;
|
|
1688
|
+
const entry = existing || {
|
|
1689
|
+
kind: 'skill',
|
|
1690
|
+
controlKey,
|
|
1691
|
+
platform: this.platform,
|
|
1692
|
+
scope: 'user',
|
|
1693
|
+
projectPath: null,
|
|
1694
|
+
sourceKey,
|
|
1695
|
+
source: {
|
|
1696
|
+
kind: 'remote',
|
|
1697
|
+
repoId: normalizedRepo.id,
|
|
1698
|
+
fullDirectory: sourceDirectory,
|
|
1699
|
+
revision: null
|
|
1700
|
+
},
|
|
1701
|
+
artifact: { root: null, contentHash: null, state: 'metadata_only', fetchedAt: null },
|
|
1702
|
+
targetDirectory: safeDirectory,
|
|
1703
|
+
cached: false,
|
|
1704
|
+
enabled: false,
|
|
1705
|
+
trust: 'pending',
|
|
1706
|
+
projection: {
|
|
1707
|
+
mode: projectionCapability.mode,
|
|
1708
|
+
state: projectionCapability.mode === 'native-copy' ? 'disabled' : 'unsupported',
|
|
1709
|
+
path: null,
|
|
1710
|
+
updatedAt: null
|
|
1711
|
+
},
|
|
1712
|
+
managed: true,
|
|
1713
|
+
lastError: null,
|
|
1714
|
+
createdAt: Date.now(),
|
|
1715
|
+
updatedAt: Date.now()
|
|
1716
|
+
};
|
|
1717
|
+
const registered = existing || this.controlService?.registerSkill?.(entry) || entry;
|
|
1718
|
+
return {
|
|
1719
|
+
status: 'pending_refresh',
|
|
1720
|
+
controlKey: registered.controlKey,
|
|
1721
|
+
cached: registered.cached,
|
|
1722
|
+
enabled: registered.enabled,
|
|
1723
|
+
trust: registered.trust,
|
|
1724
|
+
managed: true
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
let sourceFiles = files;
|
|
1729
|
+
if (!Array.isArray(sourceFiles)) {
|
|
1730
|
+
const localRoot = this.resolveStoragePath(safeDirectory).path;
|
|
1731
|
+
if (!fs.existsSync(localRoot)) throw new Error(`本地模板 Skill "${safeDirectory}" 不存在`);
|
|
1732
|
+
sourceFiles = this._collectLocalSkillFiles(localRoot);
|
|
1733
|
+
} else {
|
|
1734
|
+
sourceFiles = sourceFiles.map(file => ({
|
|
1735
|
+
relativePath: file.relativePath || file.path,
|
|
1736
|
+
content: file.content,
|
|
1737
|
+
...(file.encoding ? { encoding: file.encoding } : {}),
|
|
1738
|
+
...(file.mode ? { mode: file.mode } : {})
|
|
1739
|
+
}));
|
|
1740
|
+
}
|
|
1741
|
+
const normalized = this.formatAdapter.normalize({
|
|
1742
|
+
platform: this.platform,
|
|
1743
|
+
files: sourceFiles,
|
|
1744
|
+
sourceMetadata: { name: safeDirectory }
|
|
1745
|
+
});
|
|
1746
|
+
const sourceKey = projectPath
|
|
1747
|
+
? `template:${this.platform}:project:${projectPath}:${safeDirectory}`
|
|
1748
|
+
: `template:${this.platform}:${safeDirectory}`;
|
|
1749
|
+
const controlKey = `skill:${this.platform}:${scope}:${projectPath || 'user'}:${sourceKey}`;
|
|
1750
|
+
const scopeOptions = { scope, projectPath };
|
|
1751
|
+
const existing = this.controlService?.getSkill?.(controlKey, scopeOptions) || null;
|
|
1752
|
+
if (existing) {
|
|
1753
|
+
return {
|
|
1754
|
+
status: 'pending',
|
|
1755
|
+
controlKey: existing.controlKey,
|
|
1756
|
+
cached: existing.cached,
|
|
1757
|
+
enabled: existing.enabled,
|
|
1758
|
+
trust: existing.trust,
|
|
1759
|
+
managed: true
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
const published = this.artifactStore?.publishSkill?.({
|
|
1763
|
+
platform: this.platform,
|
|
1764
|
+
sourceKey,
|
|
1765
|
+
format: normalized.format,
|
|
1766
|
+
files: normalized.files,
|
|
1767
|
+
metadata: {
|
|
1768
|
+
name: safeDirectory,
|
|
1769
|
+
directory: safeDirectory,
|
|
1770
|
+
sourceProvider: 'template',
|
|
1771
|
+
sourceScope: scope,
|
|
1772
|
+
...(projectPath ? { projectPath } : {}),
|
|
1773
|
+
revision: null
|
|
1774
|
+
}
|
|
1775
|
+
}) || {
|
|
1776
|
+
root: null,
|
|
1777
|
+
contentHash: null,
|
|
1778
|
+
state: 'ready',
|
|
1779
|
+
fetchedAt: Date.now()
|
|
1780
|
+
};
|
|
1781
|
+
const entry = {
|
|
1782
|
+
kind: 'skill',
|
|
1783
|
+
controlKey,
|
|
1784
|
+
platform: this.platform,
|
|
1785
|
+
scope,
|
|
1786
|
+
projectPath,
|
|
1787
|
+
sourceKey,
|
|
1788
|
+
source: { kind: 'template', repoId: null, fullDirectory: safeDirectory, revision: null },
|
|
1789
|
+
artifact: {
|
|
1790
|
+
root: published.root,
|
|
1791
|
+
contentHash: published.contentHash,
|
|
1792
|
+
state: published.state || 'ready',
|
|
1793
|
+
fetchedAt: published.fetchedAt || Date.now()
|
|
1794
|
+
},
|
|
1795
|
+
targetDirectory: safeDirectory,
|
|
1796
|
+
cached: true,
|
|
1797
|
+
enabled: false,
|
|
1798
|
+
trust: 'pending',
|
|
1799
|
+
projection: {
|
|
1800
|
+
mode: projectionCapability.mode,
|
|
1801
|
+
state: projectionCapability.mode === 'native-copy' ? 'disabled' : 'unsupported',
|
|
1802
|
+
path: null,
|
|
1803
|
+
updatedAt: null
|
|
1804
|
+
},
|
|
1805
|
+
managed: true,
|
|
1806
|
+
createdAt: Date.now(),
|
|
1807
|
+
updatedAt: Date.now()
|
|
1808
|
+
};
|
|
1809
|
+
const registered = this.controlService?.registerSkill?.(entry) || entry;
|
|
1810
|
+
return {
|
|
1811
|
+
status: 'pending',
|
|
1812
|
+
controlKey: registered.controlKey,
|
|
1813
|
+
cached: registered.cached,
|
|
1814
|
+
enabled: registered.enabled,
|
|
1815
|
+
trust: registered.trust,
|
|
1816
|
+
managed: true,
|
|
1817
|
+
warnings: normalized.warnings
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
_collectLocalSkillBundles(repo) {
|
|
1822
|
+
const repoRoot = resolveExistingLocalRepoRoot(repo.localPath);
|
|
1823
|
+
const safeDirectory = normalizeSkillRelativePath(repo.directory || '', 'skill repository directory', {
|
|
1824
|
+
allowEmpty: true
|
|
1825
|
+
});
|
|
1826
|
+
const scanRoot = safeDirectory
|
|
1827
|
+
? resolveInsideRoot(repoRoot, safeDirectory, 'Skill repository directory', { allowHiddenSegments: true })
|
|
1828
|
+
: repoRoot;
|
|
1829
|
+
assertNoSymlinkComponents(repoRoot, scanRoot, fs);
|
|
1830
|
+
if (!fs.existsSync(scanRoot)) throw new Error(`Local repo path not found: ${scanRoot}`);
|
|
1831
|
+
|
|
1832
|
+
const bundles = [];
|
|
1833
|
+
const visit = currentDir => {
|
|
1834
|
+
if (fs.lstatSync(currentDir).isSymbolicLink()) {
|
|
1835
|
+
throw new Error(`Skill repository contains symlink: ${currentDir}`);
|
|
1836
|
+
}
|
|
1837
|
+
const skillMdPath = path.join(currentDir, 'SKILL.md');
|
|
1838
|
+
if (fs.existsSync(skillMdPath)) {
|
|
1839
|
+
if (fs.lstatSync(skillMdPath).isSymbolicLink()) {
|
|
1840
|
+
throw new Error(`Skill repository contains symlink: ${skillMdPath}`);
|
|
1841
|
+
}
|
|
1842
|
+
const files = this._collectLocalSkillFiles(currentDir);
|
|
1843
|
+
const skillMd = files.find(file => file.relativePath === 'SKILL.md');
|
|
1844
|
+
const metadata = this.parseSkillMd(String(skillMd.content));
|
|
1845
|
+
const fullDirectory = normalizeRepoPath(path.relative(repoRoot, currentDir));
|
|
1846
|
+
const directory = this.resolveSkillDirectory(fullDirectory, repo.directory || '', repo);
|
|
1847
|
+
bundles.push({
|
|
1848
|
+
sourceKey: this._skillBundleSourceKey(repo, fullDirectory),
|
|
1849
|
+
directory,
|
|
1850
|
+
fullDirectory,
|
|
1851
|
+
files,
|
|
1852
|
+
metadata: {
|
|
1853
|
+
name: metadata.name || directory,
|
|
1854
|
+
description: metadata.description || '',
|
|
1855
|
+
revision: repo.revision || null,
|
|
1856
|
+
repoId: repo.id,
|
|
1857
|
+
repoProvider: 'local',
|
|
1858
|
+
repoLocalPath: repo.localPath,
|
|
1859
|
+
readmeUrl: null
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
1865
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
1866
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
1867
|
+
visit(path.join(currentDir, entry.name));
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
visit(scanRoot);
|
|
1871
|
+
return bundles;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
async fetchRepoSkillBundles(repo) {
|
|
1875
|
+
if (repo.provider === 'local') {
|
|
1876
|
+
return this._collectLocalSkillBundles(repo);
|
|
1877
|
+
}
|
|
1878
|
+
const tree = repo.provider === 'gitlab'
|
|
1879
|
+
? await this.fetchGitLabTree(repo)
|
|
1880
|
+
: await this.fetchGitHubRepoTree(repo);
|
|
1881
|
+
return this._fetchGitSkillBundles(repo, tree, tree.revision || repo.revision || repo.commit || null);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
_controlEntryForBundle(bundle, repo, artifact, scope = 'user', projectPath = null) {
|
|
1885
|
+
const normalizedProjectPath = scope === 'project' ? String(projectPath || '').trim() : null;
|
|
1886
|
+
if (scope === 'project' && !normalizedProjectPath) {
|
|
1887
|
+
throw new Error('Project Skill refresh requires a canonical projectPath');
|
|
1888
|
+
}
|
|
1889
|
+
const sourceKey = bundle.sourceKey;
|
|
1890
|
+
const location = normalizedProjectPath || 'user';
|
|
1891
|
+
const controlKey = `skill:${this.platform}:${scope}:${location}:${sourceKey}`;
|
|
1892
|
+
const existing = this.controlService?.getSkill?.(controlKey, {
|
|
1893
|
+
scope,
|
|
1894
|
+
projectPath: normalizedProjectPath
|
|
1895
|
+
}) || null;
|
|
1896
|
+
const revision = bundle.metadata?.revision || null;
|
|
1897
|
+
const changed = !existing
|
|
1898
|
+
|| existing.artifact?.contentHash !== artifact.contentHash
|
|
1899
|
+
|| (existing.source?.revision || null) !== revision;
|
|
1900
|
+
let staleProjectionError = null;
|
|
1901
|
+
if (changed && (existing?.enabled || existing?.projection?.state === 'enabled') && this.controlService?.setSkillEnabled) {
|
|
1902
|
+
try {
|
|
1903
|
+
const disabled = this.controlService.setSkillEnabled({
|
|
1904
|
+
platform: this.platform,
|
|
1905
|
+
controlKey,
|
|
1906
|
+
scope,
|
|
1907
|
+
projectPath: normalizedProjectPath,
|
|
1908
|
+
enabled: false
|
|
1909
|
+
});
|
|
1910
|
+
if (disabled?.status === 'projection_failed') staleProjectionError = disabled.lastError || 'Unable to remove previous projection';
|
|
1911
|
+
} catch (error) {
|
|
1912
|
+
staleProjectionError = error.message;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
const next = {
|
|
1916
|
+
...bundle.metadata,
|
|
1917
|
+
kind: 'skill',
|
|
1918
|
+
controlKey,
|
|
1919
|
+
platform: this.platform,
|
|
1920
|
+
scope,
|
|
1921
|
+
projectPath: normalizedProjectPath,
|
|
1922
|
+
sourceKey,
|
|
1923
|
+
source: {
|
|
1924
|
+
kind: repo.provider === 'local' ? 'local' : 'remote',
|
|
1925
|
+
repoId: repo.id || null,
|
|
1926
|
+
fullDirectory: bundle.fullDirectory,
|
|
1927
|
+
revision
|
|
1928
|
+
},
|
|
1929
|
+
artifact: {
|
|
1930
|
+
root: artifact.root,
|
|
1931
|
+
contentHash: artifact.contentHash,
|
|
1932
|
+
format: artifact.format || null,
|
|
1933
|
+
state: artifact.state || 'ready',
|
|
1934
|
+
fetchedAt: artifact.fetchedAt || Date.now()
|
|
1935
|
+
},
|
|
1936
|
+
targetDirectory: bundle.directory,
|
|
1937
|
+
cached: true,
|
|
1938
|
+
enabled: changed ? false : existing.enabled === true,
|
|
1939
|
+
trust: changed ? 'needs_review' : (existing.trust || 'pending'),
|
|
1940
|
+
projection: changed
|
|
1941
|
+
? { ...(existing?.projection || {}), mode: existing?.projection?.mode || 'native-copy', state: 'disabled', updatedAt: Date.now() }
|
|
1942
|
+
: (existing?.projection || { mode: 'native-copy', state: 'disabled', updatedAt: Date.now() }),
|
|
1943
|
+
managed: true,
|
|
1944
|
+
lastError: staleProjectionError,
|
|
1945
|
+
createdAt: existing?.createdAt || Date.now(),
|
|
1946
|
+
updatedAt: Date.now()
|
|
1947
|
+
};
|
|
1948
|
+
return this.controlService?.registerSkill
|
|
1949
|
+
? this.controlService.registerSkill(next)
|
|
1950
|
+
: next;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
async refreshRemoteSkills({ platform = this.platform, scope = 'user', projectPath = null, reportProgress = () => {} } = {}) {
|
|
1954
|
+
if (platform !== this.platform) throw new Error(`SkillService platform mismatch: ${platform}`);
|
|
1955
|
+
if (!['user', 'project'].includes(scope)) throw new Error('Invalid Skill refresh scope');
|
|
1956
|
+
const normalizedProjectPath = scope === 'project' ? this._skillCwdKey({ cwd: projectPath }) : null;
|
|
1957
|
+
if (scope === 'project' && !normalizedProjectPath) {
|
|
1958
|
+
throw new Error('Project Skill refresh requires a canonical projectPath');
|
|
1959
|
+
}
|
|
1960
|
+
const repos = this.loadRepos().filter(repo => repo.enabled);
|
|
1961
|
+
const successfulSkills = [];
|
|
1962
|
+
const failedRepos = [];
|
|
1963
|
+
const updatedControlKeys = [];
|
|
1964
|
+
const results = await Promise.allSettled(repos.map(async repo => {
|
|
1965
|
+
const repoId = repo.id || buildRepoLabel(repo);
|
|
1966
|
+
reportProgress({ [repoId]: { status: 'running' } });
|
|
1967
|
+
try {
|
|
1968
|
+
const bundles = await this.fetchRepoSkillBundles(repo);
|
|
1969
|
+
for (const bundle of bundles) {
|
|
1970
|
+
const scopedSourceKey = scope === 'project'
|
|
1971
|
+
? `${bundle.sourceKey}:project:${normalizedProjectPath}`
|
|
1972
|
+
: bundle.sourceKey;
|
|
1973
|
+
const scopedBundle = scopedSourceKey === bundle.sourceKey
|
|
1974
|
+
? bundle
|
|
1975
|
+
: { ...bundle, sourceKey: scopedSourceKey };
|
|
1976
|
+
const normalized = this.formatAdapter.normalize({
|
|
1977
|
+
platform: this.platform,
|
|
1978
|
+
files: scopedBundle.files,
|
|
1979
|
+
sourceMetadata: scopedBundle.metadata
|
|
1980
|
+
});
|
|
1981
|
+
if (!this.artifactStore) throw new Error('Skill artifact store is unavailable');
|
|
1982
|
+
const artifact = await this.artifactStore.publishSkill({
|
|
1983
|
+
platform: this.platform,
|
|
1984
|
+
sourceKey: scopedBundle.sourceKey,
|
|
1985
|
+
format: normalized.format,
|
|
1986
|
+
files: normalized.files,
|
|
1987
|
+
metadata: {
|
|
1988
|
+
...scopedBundle.metadata,
|
|
1989
|
+
directory: scopedBundle.directory,
|
|
1990
|
+
fullDirectory: scopedBundle.fullDirectory,
|
|
1991
|
+
sourceProvider: repo.provider === 'local' ? 'local-repo' : 'remote',
|
|
1992
|
+
sourceScope: scope,
|
|
1993
|
+
...(normalizedProjectPath ? { projectPath: normalizedProjectPath } : {}),
|
|
1994
|
+
revision: scopedBundle.metadata?.revision || null
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
const control = this._controlEntryForBundle(
|
|
1998
|
+
scopedBundle,
|
|
1999
|
+
repo,
|
|
2000
|
+
artifact,
|
|
2001
|
+
scope,
|
|
2002
|
+
normalizedProjectPath
|
|
2003
|
+
);
|
|
2004
|
+
if (control?.controlKey) updatedControlKeys.push(control.controlKey);
|
|
2005
|
+
successfulSkills.push({ ...scopedBundle, artifact, warnings: normalized.warnings });
|
|
2006
|
+
}
|
|
2007
|
+
reportProgress({ [repoId]: { status: 'succeeded', skillCount: bundles.length } });
|
|
2008
|
+
return { repo, bundles };
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
const safeError = sanitizeRefreshError(error?.message || error);
|
|
2011
|
+
failedRepos.push({ repoId, error: safeError });
|
|
2012
|
+
reportProgress({ [repoId]: { status: 'failed', error: safeError } });
|
|
2013
|
+
throw error;
|
|
2014
|
+
}
|
|
2015
|
+
}));
|
|
2016
|
+
|
|
2017
|
+
const fetchedRepos = results.filter(result => result.status === 'fulfilled').length;
|
|
2018
|
+
const fetchedSkills = successfulSkills.length;
|
|
2019
|
+
const status = failedRepos.length === 0
|
|
2020
|
+
? 'succeeded'
|
|
2021
|
+
: (fetchedRepos > 0 || fetchedSkills > 0 ? 'partial' : 'failed');
|
|
2022
|
+
if (fetchedSkills > 0) {
|
|
2023
|
+
const fetchedAt = Date.now();
|
|
2024
|
+
const summaries = successfulSkills.map(bundle => ({
|
|
2025
|
+
...this.createSkillListItem({
|
|
2026
|
+
metadata: bundle.metadata,
|
|
2027
|
+
repo: repos.find(repo => repo.id === bundle.metadata?.repoId) || {},
|
|
2028
|
+
directory: bundle.directory,
|
|
2029
|
+
fullDirectory: bundle.fullDirectory
|
|
2030
|
+
}),
|
|
2031
|
+
sourceKey: bundle.sourceKey,
|
|
2032
|
+
sourceProvider: bundle.metadata?.repoProvider === 'local' ? 'local-repo' : 'remote',
|
|
2033
|
+
sourceScope: scope,
|
|
2034
|
+
projectPath: normalizedProjectPath,
|
|
2035
|
+
artifactRoot: bundle.artifact.root,
|
|
2036
|
+
artifactState: bundle.artifact.state || 'ready',
|
|
2037
|
+
contentHash: bundle.artifact.contentHash,
|
|
2038
|
+
revision: bundle.metadata?.revision || null,
|
|
2039
|
+
cached: true,
|
|
2040
|
+
installed: false,
|
|
2041
|
+
fetchedAt
|
|
2042
|
+
}));
|
|
2043
|
+
this._remoteSkillsCache = summaries;
|
|
2044
|
+
this._remoteSkillsFetchedAt = fetchedAt;
|
|
2045
|
+
this.saveCacheToFile(summaries, fetchedAt);
|
|
2046
|
+
}
|
|
2047
|
+
return {
|
|
2048
|
+
status,
|
|
2049
|
+
fetchedRepos,
|
|
2050
|
+
fetchedSkills,
|
|
2051
|
+
failedRepos,
|
|
2052
|
+
updatedControlKeys
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
|
|
757
2056
|
async fetchGitHubRepoSkills(repo) {
|
|
758
2057
|
const skills = [];
|
|
759
2058
|
|
|
@@ -857,13 +2156,14 @@ class SkillService {
|
|
|
857
2156
|
|
|
858
2157
|
async fetchLocalRepoSkills(repo) {
|
|
859
2158
|
const skills = [];
|
|
860
|
-
const repoRoot = repo.localPath;
|
|
2159
|
+
const repoRoot = resolveExistingLocalRepoRoot(repo.localPath);
|
|
861
2160
|
const safeDirectory = normalizeSkillRelativePath(repo.directory || '', 'skill repository directory', {
|
|
862
2161
|
allowEmpty: true
|
|
863
2162
|
});
|
|
864
2163
|
const scanRoot = safeDirectory
|
|
865
2164
|
? resolveInsideRoot(repoRoot, safeDirectory, 'Skill repository directory', { allowHiddenSegments: true })
|
|
866
2165
|
: repoRoot;
|
|
2166
|
+
assertNoSymlinkComponents(repoRoot, scanRoot, fs);
|
|
867
2167
|
|
|
868
2168
|
if (!fs.existsSync(scanRoot)) {
|
|
869
2169
|
throw new Error(`Local repo path not found: ${scanRoot}`);
|
|
@@ -901,10 +2201,16 @@ class SkillService {
|
|
|
901
2201
|
* 递归扫描外部本地仓库目录
|
|
902
2202
|
*/
|
|
903
2203
|
scanRepoLocalDir(currentDir, repoRoot, skills, repo) {
|
|
904
|
-
|
|
2204
|
+
try {
|
|
2205
|
+
if (fs.lstatSync(currentDir).isSymbolicLink()) return;
|
|
2206
|
+
} catch {
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
905
2209
|
|
|
2210
|
+
const skillMdPath = path.join(currentDir, 'SKILL.md');
|
|
906
2211
|
if (fs.existsSync(skillMdPath)) {
|
|
907
2212
|
try {
|
|
2213
|
+
if (fs.lstatSync(skillMdPath).isSymbolicLink()) return;
|
|
908
2214
|
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
909
2215
|
const metadata = this.parseSkillMd(content);
|
|
910
2216
|
const fullDirectory = normalizeRepoPath(path.relative(repoRoot, currentDir));
|
|
@@ -925,7 +2231,7 @@ class SkillService {
|
|
|
925
2231
|
try {
|
|
926
2232
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
927
2233
|
for (const entry of entries) {
|
|
928
|
-
if (!entry.isDirectory()) continue;
|
|
2234
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
929
2235
|
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
930
2236
|
this.scanRepoLocalDir(path.join(currentDir, entry.name), repoRoot, skills, repo);
|
|
931
2237
|
}
|
|
@@ -979,8 +2285,10 @@ class SkillService {
|
|
|
979
2285
|
return this.fetchGitLabFileContent(repo, file.path);
|
|
980
2286
|
}
|
|
981
2287
|
if (repo.provider === 'local') {
|
|
2288
|
+
const repoRoot = resolveExistingLocalRepoRoot(repo.localPath);
|
|
982
2289
|
const safeFilePath = normalizeSkillRelativePath(file.path, 'skill repository file path');
|
|
983
|
-
const localFilePath = resolveInsideRoot(
|
|
2290
|
+
const localFilePath = resolveInsideRoot(repoRoot, safeFilePath, 'Skill repository file path', { allowHiddenSegments: true });
|
|
2291
|
+
assertNoSymlinkComponents(repoRoot, localFilePath, fs);
|
|
984
2292
|
return fs.readFileSync(localFilePath, 'utf-8');
|
|
985
2293
|
}
|
|
986
2294
|
return this.fetchGitHubBlobContent(file.sha, repo);
|
|
@@ -991,7 +2299,7 @@ class SkillService {
|
|
|
991
2299
|
*/
|
|
992
2300
|
async fetchGitHubBlobContent(sha, repo) {
|
|
993
2301
|
const url = `https://api.github.com/repos/${repo.owner}/${repo.name}/git/blobs/${sha}`;
|
|
994
|
-
const data = await this.fetchGitHubApi(url);
|
|
2302
|
+
const data = await this.fetchGitHubApi(url, repo);
|
|
995
2303
|
if (!data || typeof data.content !== 'string') {
|
|
996
2304
|
throw new Error('Invalid GitHub blob response');
|
|
997
2305
|
}
|
|
@@ -1052,79 +2360,68 @@ class SkillService {
|
|
|
1052
2360
|
getGitHubToken(repoOrHost = DEFAULT_GITHUB_HOST) {
|
|
1053
2361
|
if (repoOrHost && typeof repoOrHost === 'object') {
|
|
1054
2362
|
const repoToken = this.resolveRepoToken(repoOrHost);
|
|
1055
|
-
if (repoToken)
|
|
1056
|
-
return repoToken;
|
|
1057
|
-
}
|
|
2363
|
+
if (repoToken) return repoToken;
|
|
1058
2364
|
}
|
|
1059
2365
|
|
|
1060
2366
|
const host = typeof repoOrHost === 'string'
|
|
1061
2367
|
? repoOrHost
|
|
1062
2368
|
: (repoOrHost?.host || DEFAULT_GITHUB_HOST);
|
|
2369
|
+
const hostname = extractHostname(host) || 'github.com';
|
|
1063
2370
|
|
|
1064
|
-
|
|
1065
|
-
if (process.env.GITHUB_TOKEN) {
|
|
1066
|
-
return process.env.GITHUB_TOKEN;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
2371
|
+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
1069
2372
|
const configToken = this.getTokenFromConfigFile('github-token.txt');
|
|
1070
|
-
if (configToken)
|
|
1071
|
-
|
|
1072
|
-
|
|
2373
|
+
if (configToken) return configToken;
|
|
2374
|
+
|
|
2375
|
+
const cached = remoteCredentialCache.get('github', hostname);
|
|
2376
|
+
if (cached.hit) return cached.value;
|
|
1073
2377
|
|
|
1074
|
-
const hostname = extractHostname(host);
|
|
1075
2378
|
if (hostname) {
|
|
1076
2379
|
const ghHostToken = this.getTokenFromCommand('gh', ['auth', 'token', '--hostname', hostname]);
|
|
1077
2380
|
if (ghHostToken) {
|
|
1078
|
-
return ghHostToken;
|
|
2381
|
+
return remoteCredentialCache.set('github', hostname, ghHostToken);
|
|
1079
2382
|
}
|
|
1080
2383
|
}
|
|
1081
2384
|
|
|
1082
2385
|
const ghToken = this.getTokenFromCommand('gh', ['auth', 'token']);
|
|
1083
2386
|
if (ghToken) {
|
|
1084
|
-
return ghToken;
|
|
2387
|
+
return remoteCredentialCache.set('github', hostname, ghToken);
|
|
1085
2388
|
}
|
|
1086
2389
|
|
|
1087
|
-
|
|
2390
|
+
const credentialToken = this.getTokenFromGitCredential(host);
|
|
2391
|
+
return remoteCredentialCache.set('github', hostname, credentialToken);
|
|
1088
2392
|
}
|
|
1089
2393
|
|
|
1090
2394
|
getGitLabToken(repoOrHost = DEFAULT_GITLAB_HOST) {
|
|
1091
2395
|
if (repoOrHost && typeof repoOrHost === 'object') {
|
|
1092
2396
|
const repoToken = this.resolveRepoToken(repoOrHost);
|
|
1093
|
-
if (repoToken)
|
|
1094
|
-
return repoToken;
|
|
1095
|
-
}
|
|
2397
|
+
if (repoToken) return repoToken;
|
|
1096
2398
|
}
|
|
1097
2399
|
|
|
1098
2400
|
const host = typeof repoOrHost === 'string'
|
|
1099
|
-
? repoOrHost
|
|
1100
|
-
: (repoOrHost?.host || DEFAULT_GITLAB_HOST);
|
|
1101
|
-
|
|
1102
|
-
if (process.env.GITLAB_TOKEN) {
|
|
1103
|
-
return process.env.GITLAB_TOKEN;
|
|
1104
|
-
}
|
|
1105
|
-
if (process.env.GITLAB_PRIVATE_TOKEN) {
|
|
1106
|
-
return process.env.GITLAB_PRIVATE_TOKEN;
|
|
1107
|
-
}
|
|
2401
|
+
? repoOrHost
|
|
2402
|
+
: (repoOrHost?.host || DEFAULT_GITLAB_HOST);
|
|
2403
|
+
const hostname = extractHostname(host) || 'gitlab.com';
|
|
1108
2404
|
|
|
2405
|
+
if (process.env.GITLAB_TOKEN) return process.env.GITLAB_TOKEN;
|
|
2406
|
+
if (process.env.GITLAB_PRIVATE_TOKEN) return process.env.GITLAB_PRIVATE_TOKEN;
|
|
1109
2407
|
const configToken = this.getTokenFromConfigFile('gitlab-token.txt');
|
|
1110
|
-
if (configToken)
|
|
1111
|
-
return configToken;
|
|
1112
|
-
}
|
|
2408
|
+
if (configToken) return configToken;
|
|
1113
2409
|
|
|
1114
|
-
const
|
|
1115
|
-
if (
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
2410
|
+
const cached = remoteCredentialCache.get('gitlab', hostname);
|
|
2411
|
+
if (cached.hit) return cached.value;
|
|
2412
|
+
|
|
2413
|
+
const glabHostToken = this.getTokenFromCommand('glab', ['auth', 'token', '--hostname', hostname]);
|
|
2414
|
+
if (glabHostToken) {
|
|
2415
|
+
return remoteCredentialCache.set('gitlab', hostname, glabHostToken);
|
|
1120
2416
|
}
|
|
1121
2417
|
|
|
1122
2418
|
const glabToken = this.getTokenFromCommand('glab', ['auth', 'token']);
|
|
1123
2419
|
if (glabToken) {
|
|
1124
|
-
return glabToken;
|
|
2420
|
+
return remoteCredentialCache.set('gitlab', hostname, glabToken);
|
|
1125
2421
|
}
|
|
1126
2422
|
|
|
1127
|
-
|
|
2423
|
+
const credentialToken = this.getTokenFromGitCredential(host);
|
|
2424
|
+
return remoteCredentialCache.set('gitlab', hostname, credentialToken);
|
|
1128
2425
|
}
|
|
1129
2426
|
|
|
1130
2427
|
/**
|
|
@@ -1174,7 +2471,13 @@ class SkillService {
|
|
|
1174
2471
|
if (tree?.truncated) {
|
|
1175
2472
|
console.warn(`[SkillService] GitHub tree truncated for ${repo.owner}/${repo.name}`);
|
|
1176
2473
|
}
|
|
1177
|
-
|
|
2474
|
+
const items = Array.isArray(tree?.tree) ? tree.tree : [];
|
|
2475
|
+
Object.defineProperty(items, 'revision', {
|
|
2476
|
+
value: tree?.sha || repo.revision || repo.commit || null,
|
|
2477
|
+
enumerable: false,
|
|
2478
|
+
configurable: true
|
|
2479
|
+
});
|
|
2480
|
+
return items;
|
|
1178
2481
|
}
|
|
1179
2482
|
|
|
1180
2483
|
async fetchGitLabApi(url, { raw = false, repo = null } = {}) {
|
|
@@ -1242,6 +2545,11 @@ class SkillService {
|
|
|
1242
2545
|
page = Number.isFinite(nextPage) && nextPage > 0 ? nextPage : 0;
|
|
1243
2546
|
}
|
|
1244
2547
|
|
|
2548
|
+
Object.defineProperty(tree, 'revision', {
|
|
2549
|
+
value: repo.revision || repo.commit || null,
|
|
2550
|
+
enumerable: false,
|
|
2551
|
+
configurable: true
|
|
2552
|
+
});
|
|
1245
2553
|
return tree;
|
|
1246
2554
|
}
|
|
1247
2555
|
|
|
@@ -1351,17 +2659,38 @@ class SkillService {
|
|
|
1351
2659
|
/**
|
|
1352
2660
|
* 获取文件内容
|
|
1353
2661
|
*/
|
|
1354
|
-
async fetchFileContent(url) {
|
|
1355
|
-
|
|
1356
|
-
|
|
2662
|
+
async fetchFileContent(url, { allowedHost = null, redirects = 0 } = {}) {
|
|
2663
|
+
const currentUrl = new URL(String(url));
|
|
2664
|
+
if (!['http:', 'https:'].includes(currentUrl.protocol)) {
|
|
2665
|
+
throw new Error('Skill file download requires HTTP(S)');
|
|
2666
|
+
}
|
|
2667
|
+
const host = allowedHost || currentUrl.host;
|
|
2668
|
+
if (currentUrl.host !== host) {
|
|
2669
|
+
throw new Error('Skill file redirect leaves the provider host');
|
|
2670
|
+
}
|
|
2671
|
+
if (redirects > 5) throw new Error('Skill file download redirected too many times');
|
|
2672
|
+
const protocol = currentUrl.protocol === 'https:' ? https : http;
|
|
1357
2673
|
|
|
1358
|
-
|
|
2674
|
+
return new Promise((resolve, reject) => {
|
|
2675
|
+
const req = protocol.get(currentUrl, {
|
|
1359
2676
|
headers: { 'User-Agent': 'cc-cli-skill-service' },
|
|
1360
2677
|
timeout: 10000
|
|
1361
2678
|
}, (res) => {
|
|
1362
|
-
// 处理重定向
|
|
1363
2679
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
1364
|
-
|
|
2680
|
+
let nextUrl;
|
|
2681
|
+
try {
|
|
2682
|
+
nextUrl = new URL(res.headers.location, currentUrl);
|
|
2683
|
+
} catch {
|
|
2684
|
+
reject(new Error('Invalid Skill file redirect'));
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
if (nextUrl.host !== host) {
|
|
2688
|
+
reject(new Error('Skill file redirect leaves the provider host'));
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
this.fetchFileContent(nextUrl.toString(), { allowedHost: host, redirects: redirects + 1 })
|
|
2692
|
+
.then(resolve)
|
|
2693
|
+
.catch(reject);
|
|
1365
2694
|
return;
|
|
1366
2695
|
}
|
|
1367
2696
|
|
|
@@ -1411,23 +2740,114 @@ class SkillService {
|
|
|
1411
2740
|
return normalizeSkillRelativePath(directory, label);
|
|
1412
2741
|
}
|
|
1413
2742
|
|
|
1414
|
-
|
|
1415
|
-
|
|
2743
|
+
resolveScopeOptions(options = {}) {
|
|
2744
|
+
const scope = options.scope || 'user';
|
|
2745
|
+
if (scope === 'user') {
|
|
2746
|
+
return {
|
|
2747
|
+
scope,
|
|
2748
|
+
cwd: '',
|
|
2749
|
+
roots: [this.installDir],
|
|
2750
|
+
writeRoot: this.installDir
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
if (scope !== 'project') {
|
|
2754
|
+
throw new Error('Invalid scope: expected "user" or "project"');
|
|
2755
|
+
}
|
|
2756
|
+
if (!options.cwd) {
|
|
2757
|
+
throw new Error('Project scope requires a valid cwd');
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
const projectRoot = assertExistingProjectRoot(options.cwd, fs);
|
|
2761
|
+
const mapping = this.registry?.resolve?.(this.platform)?.projectResources?.skills;
|
|
2762
|
+
if (!mapping) {
|
|
2763
|
+
throw new Error(`Project Skills are not supported for ${this.platform}`);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
const roots = mapping.readRoots.map(relativeRoot => resolveProjectTarget(
|
|
2767
|
+
projectRoot,
|
|
2768
|
+
relativeRoot,
|
|
2769
|
+
'project skill root',
|
|
2770
|
+
{ allowRoot: true },
|
|
2771
|
+
fs
|
|
2772
|
+
));
|
|
2773
|
+
const writeRoot = resolveProjectTarget(
|
|
2774
|
+
projectRoot,
|
|
2775
|
+
mapping.canonicalRoot,
|
|
2776
|
+
'project skill root',
|
|
2777
|
+
{ allowRoot: true },
|
|
2778
|
+
fs
|
|
2779
|
+
);
|
|
2780
|
+
return { scope, cwd: projectRoot, roots, writeRoot };
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
resolveInstallPath(directory, label = 'skill directory', options = {}) {
|
|
2784
|
+
if (options.refresh !== false) {
|
|
2785
|
+
this.refreshOmpPaths();
|
|
2786
|
+
}
|
|
1416
2787
|
const safeDirectory = this.normalizeSkillDirectory(directory, label);
|
|
2788
|
+
const scopeOptions = this.resolveScopeOptions(options);
|
|
2789
|
+
const target = resolveInsideRoot(scopeOptions.writeRoot, safeDirectory, label, { allowHiddenSegments: true });
|
|
2790
|
+
assertNoSymlinkComponents(scopeOptions.writeRoot, target, fs);
|
|
1417
2791
|
return {
|
|
1418
2792
|
safeDirectory,
|
|
1419
|
-
|
|
2793
|
+
scope: scopeOptions.scope,
|
|
2794
|
+
path: target
|
|
1420
2795
|
};
|
|
1421
2796
|
}
|
|
1422
2797
|
|
|
2798
|
+
|
|
1423
2799
|
resolveStoragePath(directory, label = 'skill directory') {
|
|
1424
2800
|
const safeDirectory = this.normalizeSkillDirectory(directory, label);
|
|
2801
|
+
const target = resolveInsideRoot(this.storageDir, safeDirectory, label, { allowHiddenSegments: true });
|
|
2802
|
+
assertNoSymlinkComponents(this.storageDir, target, fs);
|
|
1425
2803
|
return {
|
|
1426
2804
|
safeDirectory,
|
|
1427
|
-
path:
|
|
2805
|
+
path: target
|
|
1428
2806
|
};
|
|
1429
2807
|
}
|
|
1430
2808
|
|
|
2809
|
+
resolveWritableSkillPath(directory, options = {}) {
|
|
2810
|
+
if (options.scope === 'project') {
|
|
2811
|
+
return this.resolveInstallPath(directory, 'skill directory', options);
|
|
2812
|
+
}
|
|
2813
|
+
return this.resolveStoragePath(directory);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
_writeSkillFileAtomic(filePath, content, targetRoot = this.storageDir) {
|
|
2817
|
+
const parentDir = path.dirname(filePath);
|
|
2818
|
+
assertNoSymlinkComponents(targetRoot, parentDir, fs);
|
|
2819
|
+
assertNoSymlinkComponents(targetRoot, filePath, fs);
|
|
2820
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
2821
|
+
assertNoSymlinkComponents(targetRoot, parentDir, fs);
|
|
2822
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${randomUUID()}`;
|
|
2823
|
+
let descriptor;
|
|
2824
|
+
try {
|
|
2825
|
+
const flags = fs.constants.O_WRONLY
|
|
2826
|
+
| fs.constants.O_CREAT
|
|
2827
|
+
| fs.constants.O_EXCL
|
|
2828
|
+
| (fs.constants.O_NOFOLLOW || 0);
|
|
2829
|
+
descriptor = fs.openSync(tempPath, flags, 0o600);
|
|
2830
|
+
fs.writeFileSync(descriptor, content);
|
|
2831
|
+
fs.closeSync(descriptor);
|
|
2832
|
+
descriptor = undefined;
|
|
2833
|
+
const tempRealPath = fs.realpathSync(tempPath);
|
|
2834
|
+
const rootRealPath = fs.realpathSync(targetRoot);
|
|
2835
|
+
const relative = path.relative(rootRealPath, tempRealPath);
|
|
2836
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2837
|
+
throw new Error('Skill file escapes target root');
|
|
2838
|
+
}
|
|
2839
|
+
fs.renameSync(tempPath, filePath);
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
if (descriptor !== undefined) {
|
|
2842
|
+
try { fs.closeSync(descriptor); } catch (_) {}
|
|
2843
|
+
}
|
|
2844
|
+
try {
|
|
2845
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
2846
|
+
} catch (_) {}
|
|
2847
|
+
throw error;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
|
|
1431
2851
|
isProtectedSkillDirectory(directory) {
|
|
1432
2852
|
return this.platform === 'codex' && pathHasProtectedSegment(directory, ['.system']);
|
|
1433
2853
|
}
|
|
@@ -1435,15 +2855,16 @@ class SkillService {
|
|
|
1435
2855
|
/**
|
|
1436
2856
|
* 检查技能是否已安装
|
|
1437
2857
|
*/
|
|
1438
|
-
isInstalled(directory) {
|
|
2858
|
+
isInstalled(directory, options = {}) {
|
|
1439
2859
|
try {
|
|
1440
|
-
const { path: skillPath } = this.resolveInstallPath(directory);
|
|
2860
|
+
const { path: skillPath } = this.resolveInstallPath(directory, 'skill directory', options);
|
|
1441
2861
|
return fs.existsSync(path.join(skillPath, 'SKILL.md'));
|
|
1442
2862
|
} catch {
|
|
1443
2863
|
return false;
|
|
1444
2864
|
}
|
|
1445
2865
|
}
|
|
1446
2866
|
|
|
2867
|
+
|
|
1447
2868
|
/**
|
|
1448
2869
|
* 合并本地 cc-tool 托管的技能(扫描 storageDir,根据 installDir 判断安装状态)
|
|
1449
2870
|
*/
|
|
@@ -1457,20 +2878,47 @@ class SkillService {
|
|
|
1457
2878
|
/**
|
|
1458
2879
|
* 合并平台原生已安装技能(扫描 installDir)。
|
|
1459
2880
|
*/
|
|
1460
|
-
mergeInstalledSkills(skills) {
|
|
2881
|
+
mergeInstalledSkills(skills, options = {}) {
|
|
1461
2882
|
if (!fs.existsSync(this.installDir)) return;
|
|
1462
2883
|
this.scanLocalDir(this.installDir, this.installDir, skills, {
|
|
1463
2884
|
includeHiddenDirs: this.platform === 'codex',
|
|
1464
2885
|
forceInstalled: true,
|
|
1465
|
-
source: 'native-installed'
|
|
2886
|
+
source: 'native-installed',
|
|
2887
|
+
sourceScope: 'user',
|
|
2888
|
+
installOptions: { scope: 'user', refresh: false }
|
|
1466
2889
|
});
|
|
1467
2890
|
}
|
|
1468
2891
|
|
|
2892
|
+
mergeProjectSkills(skills, options = {}) {
|
|
2893
|
+
const scopeOptions = this.resolveScopeOptions(options);
|
|
2894
|
+
for (const root of scopeOptions.roots) {
|
|
2895
|
+
if (!fs.existsSync(root)) continue;
|
|
2896
|
+
this.scanLocalDir(root, root, skills, {
|
|
2897
|
+
includeHiddenDirs: true,
|
|
2898
|
+
forceInstalled: true,
|
|
2899
|
+
source: 'project-installed',
|
|
2900
|
+
sourceScope: 'project',
|
|
2901
|
+
installOptions: { scope: 'project', cwd: scopeOptions.cwd, refresh: false }
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
|
|
1469
2906
|
/**
|
|
1470
2907
|
* 递归扫描本地目录
|
|
1471
2908
|
*/
|
|
1472
2909
|
scanLocalDir(currentDir, baseDir, skills, options = {}) {
|
|
2910
|
+
try {
|
|
2911
|
+
if (fs.lstatSync(currentDir).isSymbolicLink()) return;
|
|
2912
|
+
} catch (error) {
|
|
2913
|
+
if (error.code !== 'ENOENT') return;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
1473
2916
|
const skillMdPath = path.join(currentDir, 'SKILL.md');
|
|
2917
|
+
try {
|
|
2918
|
+
if (fs.existsSync(skillMdPath) && fs.lstatSync(skillMdPath).isSymbolicLink()) return;
|
|
2919
|
+
} catch {
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
1474
2922
|
|
|
1475
2923
|
if (fs.existsSync(skillMdPath)) {
|
|
1476
2924
|
const directory = currentDir === baseDir
|
|
@@ -1479,12 +2927,16 @@ class SkillService {
|
|
|
1479
2927
|
|
|
1480
2928
|
const protectedSkill = this.isProtectedSkillDirectory(directory);
|
|
1481
2929
|
const skillSource = protectedSkill ? 'system-installed' : (options.source || 'local');
|
|
2930
|
+
const sourceScope = options.sourceScope || 'user';
|
|
2931
|
+
const sourceProvider = options.source === 'native-installed' ? 'native' : 'cc-tool';
|
|
1482
2932
|
|
|
1483
|
-
// 判断是否已安装到平台目录
|
|
1484
2933
|
let isInstalled = options.forceInstalled === true;
|
|
1485
2934
|
if (!isInstalled) {
|
|
1486
2935
|
try {
|
|
1487
|
-
isInstalled = fs.existsSync(path.join(
|
|
2936
|
+
isInstalled = fs.existsSync(path.join(
|
|
2937
|
+
this.resolveInstallPath(directory, 'skill directory', options.installOptions || {}).path,
|
|
2938
|
+
'SKILL.md'
|
|
2939
|
+
));
|
|
1488
2940
|
} catch {
|
|
1489
2941
|
isInstalled = false;
|
|
1490
2942
|
}
|
|
@@ -1493,9 +2945,15 @@ class SkillService {
|
|
|
1493
2945
|
try {
|
|
1494
2946
|
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
1495
2947
|
const metadata = this.parseSkillMd(content);
|
|
2948
|
+
const sourceKey = protectedSkill
|
|
2949
|
+
? `system:${this.platform}:${directory}`
|
|
2950
|
+
: (sourceProvider === 'native'
|
|
2951
|
+
? `native:${this.platform}:${directory}`
|
|
2952
|
+
: `local:${this.platform}:${directory}`);
|
|
1496
2953
|
|
|
1497
2954
|
const skill = {
|
|
1498
|
-
key: `local:${directory}`,
|
|
2955
|
+
key: `local:${sourceScope}:${directory}`,
|
|
2956
|
+
sourceKey,
|
|
1499
2957
|
name: metadata.name || directory,
|
|
1500
2958
|
description: metadata.description || '',
|
|
1501
2959
|
directory,
|
|
@@ -1504,8 +2962,8 @@ class SkillService {
|
|
|
1504
2962
|
source: skillSource,
|
|
1505
2963
|
protected: protectedSkill,
|
|
1506
2964
|
readonly: false,
|
|
1507
|
-
sourceProvider
|
|
1508
|
-
sourceScope
|
|
2965
|
+
sourceProvider,
|
|
2966
|
+
sourceScope,
|
|
1509
2967
|
sourcePath: skillMdPath,
|
|
1510
2968
|
shadowedSources: [],
|
|
1511
2969
|
readmeUrl: null,
|
|
@@ -1524,18 +2982,21 @@ class SkillService {
|
|
|
1524
2982
|
console.warn(`[SkillService] Parse local skill ${directory} error:`, err.message);
|
|
1525
2983
|
}
|
|
1526
2984
|
|
|
1527
|
-
return;
|
|
2985
|
+
return;
|
|
1528
2986
|
}
|
|
1529
2987
|
|
|
1530
|
-
// 递归子目录
|
|
1531
2988
|
try {
|
|
1532
2989
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1533
2990
|
for (const entry of entries) {
|
|
1534
|
-
if (
|
|
2991
|
+
if (
|
|
2992
|
+
entry.isDirectory()
|
|
2993
|
+
&& !entry.isSymbolicLink()
|
|
2994
|
+
&& (options.includeHiddenDirs || !entry.name.startsWith('.'))
|
|
2995
|
+
) {
|
|
1535
2996
|
this.scanLocalDir(path.join(currentDir, entry.name), baseDir, skills, options);
|
|
1536
2997
|
}
|
|
1537
2998
|
}
|
|
1538
|
-
} catch
|
|
2999
|
+
} catch {
|
|
1539
3000
|
// 忽略读取错误
|
|
1540
3001
|
}
|
|
1541
3002
|
}
|
|
@@ -1548,250 +3009,118 @@ class SkillService {
|
|
|
1548
3009
|
const identityMap = new Map();
|
|
1549
3010
|
const realPathMap = new Map();
|
|
1550
3011
|
const managedOverlayTargets = new Map();
|
|
3012
|
+
const artifactTargets = new Map();
|
|
3013
|
+
const priority = skill => {
|
|
3014
|
+
const isArtifact = Boolean(skill.artifactRoot || skill.artifact?.root);
|
|
3015
|
+
const sourceProvider = skill.sourceProvider || skill.source;
|
|
3016
|
+
if (!isArtifact && skill.sourceScope === 'project') return 4;
|
|
3017
|
+
if (!isArtifact && ['cc-tool', 'native', 'local'].includes(sourceProvider)) return 3;
|
|
3018
|
+
if (skill.sourceScope === 'project' || isArtifact) return 2;
|
|
3019
|
+
if (skill.sourceProvider === 'native' || skill.source === 'native-installed') return 1;
|
|
3020
|
+
if (skill.installed) return 1;
|
|
3021
|
+
return 0;
|
|
3022
|
+
};
|
|
1551
3023
|
|
|
1552
3024
|
for (const skill of skills) {
|
|
1553
3025
|
const normalizedDirectory = normalizeRepoPath(skill.directory).toLowerCase();
|
|
1554
3026
|
const identity = this.platform === 'omp'
|
|
1555
|
-
?
|
|
1556
|
-
: [
|
|
3027
|
+
? (skill.sourceKey || `${skill.sourceProvider || skill.source || 'unknown'}:${String(skill.name || skill.directory).toLowerCase()}`)
|
|
3028
|
+
: (skill.sourceKey || [
|
|
1557
3029
|
normalizedDirectory,
|
|
1558
3030
|
skill.repoId || ''
|
|
1559
|
-
].join('::');
|
|
1560
|
-
|
|
3031
|
+
].join('::'));
|
|
3032
|
+
let realPath = skill.realPath || '';
|
|
3033
|
+
if (!realPath && skill.sourcePath) {
|
|
1561
3034
|
try {
|
|
1562
|
-
|
|
3035
|
+
if (!fs.lstatSync(skill.sourcePath).isSymbolicLink()) {
|
|
3036
|
+
realPath = fs.realpathSync(skill.sourcePath);
|
|
3037
|
+
}
|
|
1563
3038
|
} catch {
|
|
1564
|
-
|
|
3039
|
+
realPath = '';
|
|
1565
3040
|
}
|
|
1566
|
-
}
|
|
1567
|
-
const
|
|
3041
|
+
}
|
|
3042
|
+
const isArtifact = Boolean(skill.artifactRoot || skill.artifact?.root);
|
|
3043
|
+
const isManagedOverlay = skill.projectionOverlay || (!skill.repoId && (
|
|
1568
3044
|
skill.sourceProvider === 'cc-tool'
|
|
1569
3045
|
|| skill.sourceProvider === 'native'
|
|
1570
3046
|
|| ['local', 'native-installed', 'system-installed'].includes(skill.source)
|
|
1571
|
-
);
|
|
3047
|
+
));
|
|
1572
3048
|
const existingIndex = identityMap.get(identity)
|
|
1573
3049
|
?? (realPath ? realPathMap.get(realPath) : undefined)
|
|
1574
|
-
?? (isManagedOverlay ? managedOverlayTargets.get(normalizedDirectory) : undefined)
|
|
3050
|
+
?? (isManagedOverlay ? managedOverlayTargets.get(normalizedDirectory) : undefined)
|
|
3051
|
+
?? (isManagedOverlay ? artifactTargets.get(normalizedDirectory) : undefined);
|
|
1575
3052
|
|
|
1576
3053
|
if (existingIndex == null) {
|
|
1577
3054
|
identityMap.set(identity, deduplicated.length);
|
|
1578
3055
|
if (realPath) realPathMap.set(realPath, deduplicated.length);
|
|
1579
|
-
if (
|
|
3056
|
+
if (isManagedOverlay && !managedOverlayTargets.has(normalizedDirectory)) {
|
|
1580
3057
|
managedOverlayTargets.set(normalizedDirectory, deduplicated.length);
|
|
1581
3058
|
}
|
|
3059
|
+
if (isArtifact && !artifactTargets.has(normalizedDirectory)) {
|
|
3060
|
+
artifactTargets.set(normalizedDirectory, deduplicated.length);
|
|
3061
|
+
}
|
|
1582
3062
|
deduplicated.push(skill);
|
|
1583
3063
|
continue;
|
|
1584
3064
|
}
|
|
1585
3065
|
|
|
1586
3066
|
const existing = deduplicated[existingIndex];
|
|
1587
|
-
const
|
|
3067
|
+
const incomingShadow = skill.sourceProvider || skill.sourcePath
|
|
1588
3068
|
? {
|
|
1589
3069
|
sourceProvider: skill.sourceProvider || skill.source || 'unknown',
|
|
1590
3070
|
sourceScope: skill.sourceScope || 'user',
|
|
1591
3071
|
sourcePath: skill.sourcePath || ''
|
|
1592
3072
|
}
|
|
1593
3073
|
: null;
|
|
1594
|
-
|
|
1595
|
-
|
|
3074
|
+
const existingShadow = existing.sourceProvider || existing.sourcePath
|
|
3075
|
+
? {
|
|
3076
|
+
sourceProvider: existing.sourceProvider || existing.source || 'unknown',
|
|
3077
|
+
sourceScope: existing.sourceScope || 'user',
|
|
3078
|
+
sourcePath: existing.sourcePath || ''
|
|
3079
|
+
}
|
|
3080
|
+
: null;
|
|
3081
|
+
if (
|
|
3082
|
+
(skill.sourceProvider === 'native' || skill.source === 'native-installed')
|
|
3083
|
+
&& existing.artifactRoot
|
|
3084
|
+
&& (existing.source === 'remote' || existing.sourceProvider === 'local-repo' || existing.repoId)
|
|
3085
|
+
&& existing.sourceScope === (skill.sourceScope || 'user')
|
|
3086
|
+
) {
|
|
3087
|
+
existing.shadowedSources = [
|
|
3088
|
+
...(existing.shadowedSources || []),
|
|
3089
|
+
...(incomingShadow ? [incomingShadow] : [])
|
|
3090
|
+
];
|
|
3091
|
+
if (skill.installed) existing.installed = true;
|
|
3092
|
+
continue;
|
|
1596
3093
|
}
|
|
1597
3094
|
|
|
1598
|
-
|
|
1599
|
-
if (shouldPreferIncoming) {
|
|
3095
|
+
if (priority(skill) > priority(existing)) {
|
|
1600
3096
|
deduplicated[existingIndex] = {
|
|
1601
|
-
...existing,
|
|
1602
3097
|
...skill,
|
|
1603
|
-
installed:
|
|
1604
|
-
shadowedSources:
|
|
3098
|
+
installed: Boolean(skill.installed || existing.installed),
|
|
3099
|
+
shadowedSources: [
|
|
3100
|
+
...(existing.shadowedSources || []),
|
|
3101
|
+
...(existingShadow ? [existingShadow] : [])
|
|
3102
|
+
]
|
|
1605
3103
|
};
|
|
1606
|
-
|
|
1607
|
-
existing.installed = true;
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
skills.splice(0, skills.length, ...deduplicated);
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
/**
|
|
1615
|
-
* 安装技能
|
|
1616
|
-
* @param {string} directory - 本地安装目录(相对于 installDir)
|
|
1617
|
-
* @param {Object} repo - 仓库配置
|
|
1618
|
-
* @param {string} [fullDirectory] - 仓库中的完整路径(可选,默认与 directory 相同)
|
|
1619
|
-
*/
|
|
1620
|
-
async installSkill(directory, repo, fullDirectory = null) {
|
|
1621
|
-
const { safeDirectory, path: dest } = this.resolveInstallPath(directory);
|
|
1622
|
-
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
1623
|
-
throw new Error('不能安装到 Codex 系统技能目录');
|
|
1624
|
-
}
|
|
1625
|
-
const normalizedRepo = this.normalizeRepoConfig(repo);
|
|
1626
|
-
|
|
1627
|
-
// 已安装则跳过
|
|
1628
|
-
if (fs.existsSync(dest)) {
|
|
1629
|
-
return { success: true, message: 'Already installed' };
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
// 使用 fullDirectory(仓库中的完整路径)或 directory(向后兼容)
|
|
1633
|
-
const sourcePath = fullDirectory || safeDirectory;
|
|
1634
|
-
const safeSourcePath = sourcePath
|
|
1635
|
-
? normalizeSkillRelativePath(sourcePath, 'skill source directory')
|
|
1636
|
-
: '';
|
|
1637
|
-
|
|
1638
|
-
if (normalizedRepo.provider === 'local') {
|
|
1639
|
-
const sourceDir = safeSourcePath
|
|
1640
|
-
? resolveInsideRoot(normalizedRepo.localPath, safeSourcePath, 'Skill source directory', { allowHiddenSegments: true })
|
|
1641
|
-
: normalizedRepo.localPath;
|
|
1642
|
-
|
|
1643
|
-
if (!fs.existsSync(sourceDir)) {
|
|
1644
|
-
throw new Error(`Skill directory not found: ${sourcePath || normalizedRepo.localPath}`);
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
1648
|
-
this.copyDirRecursive(sourceDir, dest);
|
|
1649
|
-
|
|
1650
|
-
this.clearCache({ removeFile: true });
|
|
1651
|
-
return { success: true, message: 'Installed successfully' };
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
const tempDir = path.join(os.tmpdir(), `skill-${Date.now()}`);
|
|
1655
|
-
const zipPath = path.join(tempDir, 'repo.zip');
|
|
1656
|
-
|
|
1657
|
-
try {
|
|
1658
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
1659
|
-
|
|
1660
|
-
let zipUrl = '';
|
|
1661
|
-
let zipHeaders = {};
|
|
1662
|
-
|
|
1663
|
-
if (normalizedRepo.provider === 'gitlab') {
|
|
1664
|
-
const projectId = encodeURIComponent(normalizedRepo.projectPath);
|
|
1665
|
-
zipUrl = `${normalizedRepo.host}/api/v4/projects/${projectId}/repository/archive.zip?sha=${encodeURIComponent(normalizedRepo.branch)}`;
|
|
1666
|
-
const token = this.getGitLabToken(normalizedRepo);
|
|
1667
|
-
if (token) {
|
|
1668
|
-
zipHeaders['PRIVATE-TOKEN'] = token;
|
|
1669
|
-
}
|
|
3104
|
+
if (isArtifact) artifactTargets.set(normalizedDirectory, existingIndex);
|
|
1670
3105
|
} else {
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
await this.downloadFile(zipUrl, zipPath, zipHeaders);
|
|
1680
|
-
|
|
1681
|
-
// 解压
|
|
1682
|
-
const zip = new AdmZip(zipPath);
|
|
1683
|
-
zip.extractAllTo(tempDir, true);
|
|
1684
|
-
|
|
1685
|
-
// 找到解压后的目录(GitHub ZIP 会有一个根目录)
|
|
1686
|
-
const extractedDirs = fs.readdirSync(tempDir).filter(f =>
|
|
1687
|
-
fs.statSync(path.join(tempDir, f)).isDirectory()
|
|
1688
|
-
);
|
|
1689
|
-
|
|
1690
|
-
if (extractedDirs.length === 0) {
|
|
1691
|
-
throw new Error('Empty archive');
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
const repoDir = path.join(tempDir, extractedDirs[0]);
|
|
1695
|
-
const sourceDir = safeSourcePath
|
|
1696
|
-
? resolveInsideRoot(repoDir, safeSourcePath, 'Skill source directory', { allowHiddenSegments: true })
|
|
1697
|
-
: repoDir;
|
|
1698
|
-
|
|
1699
|
-
if (!fs.existsSync(sourceDir)) {
|
|
1700
|
-
throw new Error(`Skill directory not found: ${sourcePath}`);
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
// 复制到安装目录
|
|
1704
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
1705
|
-
this.copyDirRecursive(sourceDir, dest);
|
|
1706
|
-
|
|
1707
|
-
this.clearCache({ removeFile: true });
|
|
1708
|
-
|
|
1709
|
-
return { success: true, message: 'Installed successfully' };
|
|
1710
|
-
} finally {
|
|
1711
|
-
// 清理临时目录
|
|
1712
|
-
try {
|
|
1713
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1714
|
-
} catch (e) {
|
|
1715
|
-
// 忽略清理错误
|
|
3106
|
+
existing.shadowedSources = [
|
|
3107
|
+
...(existing.shadowedSources || []),
|
|
3108
|
+
...(incomingShadow ? [incomingShadow] : [])
|
|
3109
|
+
];
|
|
3110
|
+
if (skill.installed) existing.installed = true;
|
|
1716
3111
|
}
|
|
1717
3112
|
}
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
/**
|
|
1721
|
-
* 下载文件
|
|
1722
|
-
*/
|
|
1723
|
-
async downloadFile(url, dest, headers = {}) {
|
|
1724
|
-
return new Promise((resolve, reject) => {
|
|
1725
|
-
const file = createWriteStream(dest);
|
|
1726
|
-
const transport = url.startsWith('http:') ? http : https;
|
|
1727
|
-
|
|
1728
|
-
const request = transport.get(url, {
|
|
1729
|
-
headers: {
|
|
1730
|
-
'User-Agent': 'cc-cli-skill-service',
|
|
1731
|
-
...headers
|
|
1732
|
-
},
|
|
1733
|
-
timeout: 60000
|
|
1734
|
-
}, (response) => {
|
|
1735
|
-
// 处理重定向
|
|
1736
|
-
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
1737
|
-
file.close();
|
|
1738
|
-
this.downloadFile(response.headers.location, dest, headers).then(resolve).catch(reject);
|
|
1739
|
-
return;
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
if (response.statusCode !== 200) {
|
|
1743
|
-
file.close();
|
|
1744
|
-
reject(new Error(`Download failed: HTTP ${response.statusCode}`));
|
|
1745
|
-
return;
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
response.pipe(file);
|
|
1749
|
-
file.on('finish', () => {
|
|
1750
|
-
file.close();
|
|
1751
|
-
resolve();
|
|
1752
|
-
});
|
|
1753
|
-
});
|
|
1754
3113
|
|
|
1755
|
-
|
|
1756
|
-
file.close();
|
|
1757
|
-
fs.unlink(dest, () => { });
|
|
1758
|
-
reject(err);
|
|
1759
|
-
});
|
|
1760
|
-
|
|
1761
|
-
request.on('timeout', () => {
|
|
1762
|
-
request.destroy();
|
|
1763
|
-
file.close();
|
|
1764
|
-
fs.unlink(dest, () => { });
|
|
1765
|
-
reject(new Error('Download timeout'));
|
|
1766
|
-
});
|
|
1767
|
-
});
|
|
3114
|
+
skills.splice(0, skills.length, ...deduplicated);
|
|
1768
3115
|
}
|
|
1769
3116
|
|
|
1770
|
-
/**
|
|
1771
|
-
* 递归复制目录
|
|
1772
|
-
*/
|
|
1773
|
-
copyDirRecursive(src, dest) {
|
|
1774
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
1775
|
-
|
|
1776
|
-
for (const entry of entries) {
|
|
1777
|
-
const srcPath = path.join(src, entry.name);
|
|
1778
|
-
const destPath = path.join(dest, entry.name);
|
|
1779
|
-
|
|
1780
|
-
if (entry.isDirectory()) {
|
|
1781
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
1782
|
-
this.copyDirRecursive(srcPath, destPath);
|
|
1783
|
-
} else {
|
|
1784
|
-
fs.copyFileSync(srcPath, destPath);
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
3117
|
|
|
1789
3118
|
/**
|
|
1790
3119
|
* 创建自定义技能
|
|
1791
3120
|
*/
|
|
1792
|
-
createCustomSkill({ name, directory, description, content }) {
|
|
1793
|
-
const {
|
|
1794
|
-
|
|
3121
|
+
createCustomSkill({ name, directory, description, content, scope = 'user', cwd } = {}) {
|
|
3122
|
+
const options = { scope, ...(cwd ? { cwd } : {}) };
|
|
3123
|
+
const { safeDirectory, path: dest } = this.resolveWritableSkillPath(directory, options);
|
|
1795
3124
|
// 检查是否已存在
|
|
1796
3125
|
if (fs.existsSync(dest)) {
|
|
1797
3126
|
throw new Error(`技能目录 "${directory}" 已存在`);
|
|
@@ -1812,12 +3141,24 @@ description: "${normalizedDescription}"
|
|
|
1812
3141
|
${content}
|
|
1813
3142
|
`;
|
|
1814
3143
|
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
3144
|
+
const projectRoot = scope === 'project' ? this.resolveScopeOptions(options).cwd : null;
|
|
3145
|
+
this._writeSkillFileAtomic(
|
|
3146
|
+
path.join(dest, 'SKILL.md'),
|
|
3147
|
+
skillMdContent,
|
|
3148
|
+
projectRoot || this.storageDir
|
|
3149
|
+
);
|
|
3150
|
+
const control = this._registerCreatedSkill({
|
|
3151
|
+
root: dest,
|
|
3152
|
+
directory: safeDirectory,
|
|
3153
|
+
scope,
|
|
3154
|
+
cwd
|
|
3155
|
+
});
|
|
3156
|
+
if (scope === 'project') {
|
|
3157
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
3158
|
+
}
|
|
1818
3159
|
this.clearCache({ removeFile: true });
|
|
1819
3160
|
|
|
1820
|
-
return { success: true, message: '技能创建成功', directory: safeDirectory };
|
|
3161
|
+
return { success: true, message: '技能创建成功', directory: safeDirectory, ...control };
|
|
1821
3162
|
}
|
|
1822
3163
|
|
|
1823
3164
|
/**
|
|
@@ -1826,8 +3167,9 @@ ${content}
|
|
|
1826
3167
|
* @param {Array<{path: string, content: string}>} files - 文件数组
|
|
1827
3168
|
* @returns {Object} 创建结果
|
|
1828
3169
|
*/
|
|
1829
|
-
createSkillWithFiles({ directory, files }) {
|
|
1830
|
-
const {
|
|
3170
|
+
createSkillWithFiles({ directory, files, scope = 'user', cwd } = {}) {
|
|
3171
|
+
const options = { scope, ...(cwd ? { cwd } : {}) };
|
|
3172
|
+
const { safeDirectory, path: dest } = this.resolveWritableSkillPath(directory, options);
|
|
1831
3173
|
|
|
1832
3174
|
// 检查是否已存在
|
|
1833
3175
|
if (fs.existsSync(dest)) {
|
|
@@ -1849,32 +3191,33 @@ ${content}
|
|
|
1849
3191
|
// 创建目录
|
|
1850
3192
|
fs.mkdirSync(dest, { recursive: true });
|
|
1851
3193
|
|
|
1852
|
-
|
|
3194
|
+
const projectRoot = scope === 'project' ? this.resolveScopeOptions(options).cwd : null;
|
|
3195
|
+
const targetRoot = projectRoot || this.storageDir;
|
|
1853
3196
|
for (const file of safeFiles) {
|
|
1854
3197
|
const filePath = resolveInsideRoot(dest, file.safePath, 'skill file path', { allowHiddenSegments: true });
|
|
1855
|
-
const
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
fs.mkdirSync(fileDir, { recursive: true });
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
// 写入文件内容
|
|
1863
|
-
if (file.isBase64) {
|
|
1864
|
-
// 二进制文件使用 base64 编码
|
|
1865
|
-
fs.writeFileSync(filePath, Buffer.from(file.content, 'base64'));
|
|
1866
|
-
} else {
|
|
1867
|
-
fs.writeFileSync(filePath, file.content, 'utf-8');
|
|
1868
|
-
}
|
|
3198
|
+
const fileContent = file.isBase64
|
|
3199
|
+
? Buffer.from(file.content, 'base64')
|
|
3200
|
+
: Buffer.from(file.content, 'utf8');
|
|
3201
|
+
this._writeSkillFileAtomic(filePath, fileContent, targetRoot);
|
|
1869
3202
|
}
|
|
1870
3203
|
|
|
3204
|
+
const control = this._registerCreatedSkill({
|
|
3205
|
+
root: dest,
|
|
3206
|
+
directory: safeDirectory,
|
|
3207
|
+
scope,
|
|
3208
|
+
cwd
|
|
3209
|
+
});
|
|
3210
|
+
if (scope === 'project') {
|
|
3211
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
3212
|
+
}
|
|
1871
3213
|
this.clearCache({ removeFile: true });
|
|
1872
3214
|
|
|
1873
3215
|
return {
|
|
1874
3216
|
success: true,
|
|
1875
3217
|
message: '技能创建成功',
|
|
1876
3218
|
directory: safeDirectory,
|
|
1877
|
-
fileCount: files.length
|
|
3219
|
+
fileCount: files.length,
|
|
3220
|
+
...control
|
|
1878
3221
|
};
|
|
1879
3222
|
}
|
|
1880
3223
|
|
|
@@ -1883,8 +3226,8 @@ ${content}
|
|
|
1883
3226
|
* @param {string} directory - 技能目录名
|
|
1884
3227
|
* @returns {Array<{path: string, size: number, isDirectory: boolean}>}
|
|
1885
3228
|
*/
|
|
1886
|
-
getSkillFiles(directory) {
|
|
1887
|
-
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory);
|
|
3229
|
+
getSkillFiles(directory, options = {}) {
|
|
3230
|
+
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory, 'skill directory', options);
|
|
1888
3231
|
|
|
1889
3232
|
if (!fs.existsSync(skillPath)) {
|
|
1890
3233
|
throw new Error(`技能 "${safeDirectory}" 不存在`);
|
|
@@ -1899,9 +3242,15 @@ ${content}
|
|
|
1899
3242
|
* 递归扫描目录获取文件列表
|
|
1900
3243
|
*/
|
|
1901
3244
|
_scanFilesRecursive(currentDir, baseDir, files) {
|
|
3245
|
+
try {
|
|
3246
|
+
if (fs.lstatSync(currentDir).isSymbolicLink()) return;
|
|
3247
|
+
} catch {
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
1902
3250
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1903
3251
|
|
|
1904
3252
|
for (const entry of entries) {
|
|
3253
|
+
if (entry.isSymbolicLink()) continue;
|
|
1905
3254
|
const fullPath = path.join(currentDir, entry.name);
|
|
1906
3255
|
const relativePath = path.relative(baseDir, fullPath);
|
|
1907
3256
|
|
|
@@ -1912,7 +3261,7 @@ ${content}
|
|
|
1912
3261
|
isDirectory: true
|
|
1913
3262
|
});
|
|
1914
3263
|
this._scanFilesRecursive(fullPath, baseDir, files);
|
|
1915
|
-
} else {
|
|
3264
|
+
} else if (entry.isFile()) {
|
|
1916
3265
|
const stats = fs.statSync(fullPath);
|
|
1917
3266
|
files.push({
|
|
1918
3267
|
path: relativePath,
|
|
@@ -1929,14 +3278,18 @@ ${content}
|
|
|
1929
3278
|
* @param {string} filePath - 文件相对路径
|
|
1930
3279
|
* @returns {Object} 文件内容
|
|
1931
3280
|
*/
|
|
1932
|
-
getSkillFileContent(directory, filePath) {
|
|
1933
|
-
const { path: skillPath } = this.resolveInstallPath(directory);
|
|
3281
|
+
getSkillFileContent(directory, filePath, options = {}) {
|
|
3282
|
+
const { path: skillPath } = this.resolveInstallPath(directory, 'skill directory', options);
|
|
1934
3283
|
const safeFilePath = normalizeSkillRelativePath(filePath, 'skill file path');
|
|
1935
3284
|
const fullPath = resolveInsideRoot(skillPath, safeFilePath, 'skill file path', { allowHiddenSegments: true });
|
|
3285
|
+
assertNoSymlinkComponents(skillPath, fullPath, fs);
|
|
1936
3286
|
|
|
1937
3287
|
if (!fs.existsSync(fullPath)) {
|
|
1938
3288
|
throw new Error(`文件 "${safeFilePath}" 不存在`);
|
|
1939
3289
|
}
|
|
3290
|
+
if (fs.lstatSync(fullPath).isSymbolicLink()) {
|
|
3291
|
+
throw new Error(`skill file path contains symlink: ${safeFilePath}`);
|
|
3292
|
+
}
|
|
1940
3293
|
|
|
1941
3294
|
const stats = fs.statSync(fullPath);
|
|
1942
3295
|
if (stats.isDirectory()) {
|
|
@@ -1970,8 +3323,8 @@ ${content}
|
|
|
1970
3323
|
* @param {string} directory - 技能目录名
|
|
1971
3324
|
* @param {Array<{path: string, content: string, isBase64?: boolean}>} files - 文件数组
|
|
1972
3325
|
*/
|
|
1973
|
-
addSkillFiles(directory, files) {
|
|
1974
|
-
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory);
|
|
3326
|
+
addSkillFiles(directory, files, options = {}) {
|
|
3327
|
+
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory, 'skill directory', options);
|
|
1975
3328
|
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
1976
3329
|
throw new Error('不能修改 Codex 系统技能');
|
|
1977
3330
|
}
|
|
@@ -1980,23 +3333,22 @@ ${content}
|
|
|
1980
3333
|
throw new Error(`技能 "${safeDirectory}" 不存在`);
|
|
1981
3334
|
}
|
|
1982
3335
|
|
|
3336
|
+
const projectRoot = options.scope === 'project' ? this.resolveScopeOptions(options).cwd : null;
|
|
3337
|
+
const targetRoot = projectRoot || this.installDir;
|
|
3338
|
+
|
|
1983
3339
|
const added = [];
|
|
1984
3340
|
for (const file of files) {
|
|
1985
3341
|
const safeFilePath = normalizeSkillRelativePath(file.path, 'skill file path');
|
|
1986
3342
|
const filePath = resolveInsideRoot(skillPath, safeFilePath, 'skill file path', { allowHiddenSegments: true });
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
if (!fs.existsSync(fileDir)) {
|
|
1991
|
-
fs.mkdirSync(fileDir, { recursive: true });
|
|
3343
|
+
assertNoSymlinkComponents(targetRoot, filePath, fs);
|
|
3344
|
+
if (fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink()) {
|
|
3345
|
+
throw new Error(`skill file path contains symlink: ${safeFilePath}`);
|
|
1992
3346
|
}
|
|
1993
3347
|
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
fs.writeFileSync(filePath, file.content, 'utf-8');
|
|
1999
|
-
}
|
|
3348
|
+
const fileContent = file.isBase64
|
|
3349
|
+
? Buffer.from(file.content, 'base64')
|
|
3350
|
+
: Buffer.from(file.content, 'utf8');
|
|
3351
|
+
this._writeSkillFileAtomic(filePath, fileContent, targetRoot);
|
|
2000
3352
|
added.push(safeFilePath);
|
|
2001
3353
|
}
|
|
2002
3354
|
|
|
@@ -2010,8 +3362,8 @@ ${content}
|
|
|
2010
3362
|
* @param {string} directory - 技能目录名
|
|
2011
3363
|
* @param {string} filePath - 文件相对路径
|
|
2012
3364
|
*/
|
|
2013
|
-
deleteSkillFile(directory, filePath) {
|
|
2014
|
-
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory);
|
|
3365
|
+
deleteSkillFile(directory, filePath, options = {}) {
|
|
3366
|
+
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory, 'skill directory', options);
|
|
2015
3367
|
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
2016
3368
|
throw new Error('不能修改 Codex 系统技能');
|
|
2017
3369
|
}
|
|
@@ -2030,11 +3382,14 @@ ${content}
|
|
|
2030
3382
|
throw new Error('不能删除 SKILL.md 文件');
|
|
2031
3383
|
}
|
|
2032
3384
|
const fullPath = resolveInsideRoot(skillPath, safeFilePath, 'skill file path', { allowHiddenSegments: true });
|
|
3385
|
+
assertNoSymlinkComponents(skillPath, fullPath, fs);
|
|
2033
3386
|
|
|
2034
3387
|
if (!fs.existsSync(fullPath)) {
|
|
2035
3388
|
throw new Error(`文件 "${safeFilePath}" 不存在`);
|
|
2036
3389
|
}
|
|
2037
|
-
|
|
3390
|
+
if (fs.lstatSync(fullPath).isSymbolicLink()) {
|
|
3391
|
+
throw new Error(`skill file path contains symlink: ${safeFilePath}`);
|
|
3392
|
+
}
|
|
2038
3393
|
const stats = fs.statSync(fullPath);
|
|
2039
3394
|
if (stats.isDirectory()) {
|
|
2040
3395
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
@@ -2054,8 +3409,8 @@ ${content}
|
|
|
2054
3409
|
* @param {string} content - 新内容
|
|
2055
3410
|
* @param {boolean} isBase64 - 是否为 base64 编码
|
|
2056
3411
|
*/
|
|
2057
|
-
updateSkillFile(directory, filePath, content, isBase64 = false) {
|
|
2058
|
-
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory);
|
|
3412
|
+
updateSkillFile(directory, filePath, content, isBase64 = false, options = {}) {
|
|
3413
|
+
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory, 'skill directory', options);
|
|
2059
3414
|
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
2060
3415
|
throw new Error('不能修改 Codex 系统技能');
|
|
2061
3416
|
}
|
|
@@ -2066,263 +3421,251 @@ ${content}
|
|
|
2066
3421
|
|
|
2067
3422
|
const safeFilePath = normalizeSkillRelativePath(filePath, 'skill file path');
|
|
2068
3423
|
const fullPath = resolveInsideRoot(skillPath, safeFilePath, 'skill file path', { allowHiddenSegments: true });
|
|
3424
|
+
assertNoSymlinkComponents(skillPath, fullPath, fs);
|
|
2069
3425
|
|
|
2070
3426
|
if (!fs.existsSync(fullPath)) {
|
|
2071
3427
|
throw new Error(`文件 "${safeFilePath}" 不存在`);
|
|
2072
3428
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
fs.writeFileSync(fullPath, Buffer.from(content, 'base64'));
|
|
2076
|
-
} else {
|
|
2077
|
-
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
3429
|
+
if (fs.lstatSync(fullPath).isSymbolicLink()) {
|
|
3430
|
+
throw new Error(`skill file path contains symlink: ${safeFilePath}`);
|
|
2078
3431
|
}
|
|
2079
3432
|
|
|
3433
|
+
const fileContent = isBase64
|
|
3434
|
+
? Buffer.from(content, 'base64')
|
|
3435
|
+
: Buffer.from(content, 'utf8');
|
|
3436
|
+
const projectRoot = options.scope === 'project' ? this.resolveScopeOptions(options).cwd : null;
|
|
3437
|
+
this._writeSkillFileAtomic(fullPath, fileContent, projectRoot || this.installDir);
|
|
3438
|
+
|
|
2080
3439
|
this.clearCache({ removeFile: true });
|
|
2081
3440
|
|
|
2082
3441
|
return { success: true, updated: safeFilePath };
|
|
2083
3442
|
}
|
|
2084
3443
|
|
|
2085
3444
|
|
|
2086
|
-
/**
|
|
2087
|
-
* 安装 cc-tool 本地托管的技能(从 storageDir cp 到 installDir)
|
|
2088
|
-
*/
|
|
2089
|
-
installLocalSkill(directory) {
|
|
2090
|
-
const { safeDirectory, path: src } = this.resolveStoragePath(directory);
|
|
2091
|
-
const { path: dest } = this.resolveInstallPath(safeDirectory);
|
|
2092
|
-
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
2093
|
-
throw new Error('不能安装到 Codex 系统技能目录');
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
if (!fs.existsSync(src)) {
|
|
2097
|
-
throw new Error(`本地技能 "${safeDirectory}" 不存在`);
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
if (fs.existsSync(dest)) {
|
|
2101
|
-
return { success: true, message: 'Already installed' };
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
2105
|
-
this.copyDirRecursive(src, dest);
|
|
2106
|
-
this.clearCache({ removeFile: true });
|
|
2107
|
-
return { success: true, message: 'Installed successfully' };
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
/**
|
|
2111
|
-
* 卸载技能
|
|
2112
|
-
*/
|
|
2113
|
-
uninstallSkill(directory, options = {}) {
|
|
2114
|
-
const safeDirectory = this.normalizeSkillDirectory(directory);
|
|
2115
|
-
const projectInstallDir = this.platform === 'omp' && options.scope === 'project' && options.cwd
|
|
2116
|
-
? path.join(path.resolve(options.cwd), '.omp', 'skills')
|
|
2117
|
-
: null;
|
|
2118
|
-
const dest = projectInstallDir
|
|
2119
|
-
? resolveInsideRoot(projectInstallDir, safeDirectory, 'skill directory', {
|
|
2120
|
-
allowHiddenSegments: true
|
|
2121
|
-
})
|
|
2122
|
-
: this.resolveInstallPath(safeDirectory).path;
|
|
2123
|
-
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
2124
|
-
throw new Error('不能卸载 Codex 系统技能');
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
if (fs.existsSync(dest)) {
|
|
2128
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
2129
|
-
this.clearCache({ removeFile: true });
|
|
2130
|
-
return { success: true, message: 'Uninstalled successfully' };
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
return { success: true, message: 'Not installed' };
|
|
2134
|
-
}
|
|
2135
3445
|
|
|
2136
3446
|
/**
|
|
2137
|
-
*
|
|
3447
|
+
* 获取本地 Skill 详情。远端内容只能来自已经发布的 artifact。
|
|
2138
3448
|
*/
|
|
2139
|
-
async getSkillDetail(directory, repoHint = null, fullDirectoryHint = '') {
|
|
3449
|
+
async getSkillDetail(directory, repoHint = null, fullDirectoryHint = '', options = {}) {
|
|
2140
3450
|
const safeDirectory = this.normalizeSkillDirectory(directory);
|
|
2141
|
-
|
|
2142
|
-
const localSkillDir = this.resolveInstallPath(safeDirectory).path;
|
|
2143
|
-
const localPath = path.join(localSkillDir, 'SKILL.md');
|
|
2144
|
-
|
|
2145
|
-
if (fs.existsSync(localPath)) {
|
|
2146
|
-
const content = fs.readFileSync(localPath, 'utf-8');
|
|
2147
|
-
const metadata = this.parseSkillMd(content);
|
|
2148
|
-
|
|
2149
|
-
// 提取正文内容(去除 frontmatter)
|
|
2150
|
-
const bodyMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
|
|
2151
|
-
const body = bodyMatch ? bodyMatch[1].trim() : content;
|
|
2152
|
-
|
|
2153
|
-
return {
|
|
2154
|
-
directory: safeDirectory,
|
|
2155
|
-
name: metadata.name || safeDirectory,
|
|
2156
|
-
description: metadata.description || '',
|
|
2157
|
-
content: body,
|
|
2158
|
-
fullContent: content,
|
|
2159
|
-
installed: true,
|
|
2160
|
-
path: localSkillDir,
|
|
2161
|
-
fullPath: localPath,
|
|
2162
|
-
installPath: localSkillDir,
|
|
2163
|
-
source: this.isProtectedSkillDirectory(safeDirectory) ? 'system-installed' : 'local',
|
|
2164
|
-
protected: this.isProtectedSkillDirectory(safeDirectory)
|
|
2165
|
-
};
|
|
2166
|
-
}
|
|
3451
|
+
const scope = options.scope || 'user';
|
|
2167
3452
|
|
|
2168
|
-
const
|
|
2169
|
-
const
|
|
3453
|
+
const readDetail = (root, metadata = {}, controlEntry = null) => {
|
|
3454
|
+
const skillPath = path.resolve(root);
|
|
3455
|
+
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
3456
|
+
if (!fs.existsSync(skillFile) || fs.lstatSync(skillFile).isSymbolicLink()) {
|
|
3457
|
+
throw new Error('Skill detail contains an unavailable or symlinked SKILL.md');
|
|
3458
|
+
}
|
|
3459
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
3460
|
+
const parsed = this.parseSkillMd(content);
|
|
2170
3461
|
const bodyMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
|
|
2171
3462
|
const body = bodyMatch ? bodyMatch[1].trim() : content;
|
|
2172
|
-
|
|
3463
|
+
const enabled = controlEntry?.enabled === true || (
|
|
3464
|
+
controlEntry == null && metadata.enabled === true
|
|
3465
|
+
);
|
|
2173
3466
|
return {
|
|
2174
3467
|
directory: safeDirectory,
|
|
2175
|
-
name: metadata.name || safeDirectory,
|
|
2176
|
-
description: metadata.description || '',
|
|
3468
|
+
name: parsed.name || metadata.name || safeDirectory,
|
|
3469
|
+
description: parsed.description || metadata.description || '',
|
|
2177
3470
|
content: body,
|
|
2178
3471
|
fullContent: content,
|
|
2179
|
-
installed:
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
3472
|
+
installed: enabled,
|
|
3473
|
+
enabled,
|
|
3474
|
+
cached: metadata.cached !== false,
|
|
3475
|
+
trust: controlEntry?.trust || metadata.trust || (metadata.source === 'remote' ? 'pending' : 'approved'),
|
|
3476
|
+
managed: true,
|
|
3477
|
+
controlKey: controlEntry?.controlKey || metadata.controlKey || null,
|
|
3478
|
+
scope: controlEntry?.scope || scope,
|
|
3479
|
+
sourceScope: controlEntry?.scope || metadata.sourceScope || scope,
|
|
3480
|
+
path: skillPath,
|
|
3481
|
+
fullPath: skillFile,
|
|
3482
|
+
installPath: skillPath,
|
|
3483
|
+
source: metadata.source || 'local',
|
|
3484
|
+
sourceProvider: metadata.sourceProvider || null,
|
|
3485
|
+
sourceKey: metadata.sourceKey || null,
|
|
3486
|
+
repoProvider: metadata.repoProvider || null,
|
|
3487
|
+
repoOwner: metadata.repoOwner || null,
|
|
3488
|
+
repoName: metadata.repoName || null,
|
|
3489
|
+
repoBranch: metadata.repoBranch || null,
|
|
3490
|
+
repoDirectory: metadata.repoDirectory || '',
|
|
3491
|
+
repoHost: metadata.repoHost || null,
|
|
3492
|
+
repoProjectPath: metadata.repoProjectPath || null,
|
|
3493
|
+
repoLocalPath: metadata.repoLocalPath || null,
|
|
3494
|
+
repoId: metadata.repoId || null,
|
|
3495
|
+
repoUrl: metadata.repoUrl || null,
|
|
3496
|
+
protected: this.isProtectedSkillDirectory(safeDirectory),
|
|
3497
|
+
readonly: metadata.readonly === true
|
|
2193
3498
|
};
|
|
2194
3499
|
};
|
|
2195
3500
|
|
|
2196
|
-
const
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
:
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
const normalized = normalizeRepoPath(candidate);
|
|
2232
|
-
if (normalized) candidateDirs.add(normalized);
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
if (repo.directory) {
|
|
2236
|
-
candidateDirs.add(normalizeRepoPath(`${repo.directory}/${normalizedDirectory}`));
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
let skillFile = null;
|
|
2240
|
-
for (const candidateDir of candidateDirs) {
|
|
2241
|
-
skillFile = treeItems.find(item =>
|
|
2242
|
-
item.type === 'blob' && (
|
|
2243
|
-
candidateDir
|
|
2244
|
-
? item.path === `${candidateDir}/SKILL.md`
|
|
2245
|
-
: item.path === 'SKILL.md'
|
|
2246
|
-
)
|
|
2247
|
-
);
|
|
2248
|
-
if (skillFile) break;
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
if (!skillFile) return null;
|
|
2252
|
-
|
|
2253
|
-
const content = await this.fetchSkillFileContent(repo, skillFile);
|
|
2254
|
-
const fullDirectory = normalizeRepoPath(skillFile.path.replace(/(^|\/)SKILL\.md$/, ''));
|
|
2255
|
-
return parseRemoteSkillContent(content, repo, fullDirectory);
|
|
2256
|
-
} catch (err) {
|
|
2257
|
-
console.warn('[SkillService] Fetch remote skill detail error:', err.message);
|
|
2258
|
-
return null;
|
|
3501
|
+
const previousSkillsCache = Array.isArray(this.skillsCache)
|
|
3502
|
+
? this.skillsCache.map(skill => ({ ...skill }))
|
|
3503
|
+
: null;
|
|
3504
|
+
const scanResult = await this.scanSkills({
|
|
3505
|
+
...options,
|
|
3506
|
+
scope,
|
|
3507
|
+
...(scope === 'project' && options.cwd ? { cwd: options.cwd } : {})
|
|
3508
|
+
});
|
|
3509
|
+
const candidates = (scanResult.skills || []).filter(skill => (
|
|
3510
|
+
normalizeRepoPath(skill.directory) === normalizeRepoPath(safeDirectory)
|
|
3511
|
+
|| (fullDirectoryHint && normalizeRepoPath(skill.fullDirectory) === normalizeRepoPath(fullDirectoryHint))
|
|
3512
|
+
));
|
|
3513
|
+
const hinted = candidates.find(skill => (
|
|
3514
|
+
repoHint && (
|
|
3515
|
+
(repoHint.id && skill.repoId === repoHint.id)
|
|
3516
|
+
|| (repoHint.owner && repoHint.name && skill.repoOwner === repoHint.owner && skill.repoName === repoHint.name)
|
|
3517
|
+
)
|
|
3518
|
+
));
|
|
3519
|
+
const selected = hinted || candidates[0];
|
|
3520
|
+
if (selected) {
|
|
3521
|
+
const selectedScope = selected.scope || (selected.sourceScope === 'project' ? 'project' : 'user');
|
|
3522
|
+
const selectedProjectPath = selectedScope === 'project'
|
|
3523
|
+
? (selected.projectPath || this._skillCwdKey(options))
|
|
3524
|
+
: null;
|
|
3525
|
+
const controlEntry = selected.controlKey
|
|
3526
|
+
? this.controlService?.getSkill?.(selected.controlKey, {
|
|
3527
|
+
scope: selectedScope,
|
|
3528
|
+
projectPath: selectedProjectPath
|
|
3529
|
+
})
|
|
3530
|
+
: null;
|
|
3531
|
+
const root = selected.artifact?.root
|
|
3532
|
+
|| selected.artifactRoot
|
|
3533
|
+
|| (selected.sourcePath ? path.dirname(selected.sourcePath) : null);
|
|
3534
|
+
if (root && fs.existsSync(root) && !fs.lstatSync(root).isSymbolicLink()) {
|
|
3535
|
+
return readDetail(root, selected, controlEntry);
|
|
2259
3536
|
}
|
|
2260
|
-
}
|
|
3537
|
+
}
|
|
2261
3538
|
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
3539
|
+
const localSkillDir = this.resolveInstallPath(safeDirectory, 'skill directory', options).path;
|
|
3540
|
+
const localPath = path.join(localSkillDir, 'SKILL.md');
|
|
3541
|
+
if (fs.existsSync(localPath) && !fs.lstatSync(localPath).isSymbolicLink()) {
|
|
3542
|
+
const projectPath = scope === 'project' ? this._skillCwdKey(options) : null;
|
|
3543
|
+
const sourceKey = projectPath
|
|
3544
|
+
? `native:${this.platform}:${safeDirectory}:project:${projectPath}`
|
|
3545
|
+
: `native:${this.platform}:${safeDirectory}`;
|
|
3546
|
+
const controlKey = `skill:${this.platform}:${scope}:${projectPath || 'user'}:${sourceKey}`;
|
|
3547
|
+
const controlEntry = this.controlService?.getSkill?.(controlKey, {
|
|
3548
|
+
scope,
|
|
3549
|
+
projectPath
|
|
3550
|
+
}) || null;
|
|
3551
|
+
return readDetail(localSkillDir, {
|
|
3552
|
+
source: scope === 'project' ? 'project' : 'native',
|
|
3553
|
+
sourceProvider: 'native',
|
|
3554
|
+
sourceScope: scope,
|
|
3555
|
+
enabled: true,
|
|
3556
|
+
cached: true,
|
|
3557
|
+
readonly: false
|
|
3558
|
+
}, controlEntry);
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
const localStoragePath = this.resolveStoragePath(safeDirectory).path;
|
|
3562
|
+
const localStorageSkillPath = path.join(localStoragePath, 'SKILL.md');
|
|
3563
|
+
if (fs.existsSync(localStorageSkillPath) && !fs.lstatSync(localStorageSkillPath).isSymbolicLink()) {
|
|
3564
|
+
const sourceKey = `local:${this.platform}:${safeDirectory}`;
|
|
3565
|
+
const controlKey = `skill:${this.platform}:user:user:${sourceKey}`;
|
|
3566
|
+
const controlEntry = this.controlService?.getSkill?.(controlKey, { scope: 'user' }) || null;
|
|
3567
|
+
return readDetail(localStoragePath, {
|
|
3568
|
+
source: 'local',
|
|
3569
|
+
sourceProvider: 'cc-tool',
|
|
3570
|
+
sourceScope: 'user',
|
|
3571
|
+
sourceKey
|
|
3572
|
+
}, controlEntry);
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
const cachedSkills = previousSkillsCache || this.skillsCache;
|
|
3576
|
+
const cachedLocalSkill = Array.isArray(cachedSkills)
|
|
3577
|
+
? cachedSkills.find(skill => (
|
|
3578
|
+
normalizeRepoPath(skill.directory) === normalizeRepoPath(safeDirectory)
|
|
3579
|
+
&& (skill.sourcePath || skill.realPath)
|
|
3580
|
+
&& !(skill.repoOwner || skill.repoProjectPath || skill.repoLocalPath)
|
|
3581
|
+
))
|
|
3582
|
+
: null;
|
|
3583
|
+
const cachedSkillPath = cachedLocalSkill?.realPath || cachedLocalSkill?.sourcePath;
|
|
3584
|
+
if (cachedSkillPath && fs.existsSync(cachedSkillPath) && !fs.lstatSync(cachedSkillPath).isSymbolicLink()) {
|
|
3585
|
+
return readDetail(path.dirname(cachedSkillPath), {
|
|
3586
|
+
...cachedLocalSkill,
|
|
3587
|
+
source: cachedLocalSkill.source || 'provider-installed',
|
|
3588
|
+
enabled: cachedLocalSkill.enabled !== false && cachedLocalSkill.installed !== false,
|
|
3589
|
+
cached: true,
|
|
3590
|
+
readonly: cachedLocalSkill.readonly === true
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
const requestedProjectPath = scope === 'project' ? this._skillCwdKey(options) : null;
|
|
3595
|
+
const artifact = this.artifactStore?.list?.({ platform: this.platform }).find(item => {
|
|
3596
|
+
const artifactScope = item?.sourceScope || 'user';
|
|
3597
|
+
if (artifactScope === 'project') {
|
|
3598
|
+
if (scope !== 'project' || item.projectPath !== requestedProjectPath) return false;
|
|
3599
|
+
} else if (item.projectPath && item.projectPath !== requestedProjectPath) {
|
|
3600
|
+
return false;
|
|
2273
3601
|
}
|
|
3602
|
+
const itemDirectory = item.targetDirectory || item.directory || item.fullDirectory || '';
|
|
3603
|
+
return normalizeRepoPath(itemDirectory) === normalizeRepoPath(safeDirectory)
|
|
3604
|
+
|| (fullDirectoryHint && normalizeRepoPath(item.fullDirectory) === normalizeRepoPath(fullDirectoryHint));
|
|
3605
|
+
});
|
|
3606
|
+
if (artifact) {
|
|
3607
|
+
const sourceKey = artifact.sourceKey || '';
|
|
3608
|
+
const artifactScope = artifact.sourceScope || 'user';
|
|
3609
|
+
const artifactProjectPath = artifactScope === 'project' ? artifact.projectPath : null;
|
|
3610
|
+
const controlKey = `skill:${this.platform}:${artifactScope}:${artifactProjectPath || 'user'}:${sourceKey}`;
|
|
3611
|
+
const controlEntry = this.controlService?.getSkill?.(controlKey, {
|
|
3612
|
+
scope: artifactScope,
|
|
3613
|
+
projectPath: artifactProjectPath
|
|
3614
|
+
}) || null;
|
|
3615
|
+
return readDetail(artifact.root, {
|
|
3616
|
+
...artifact,
|
|
3617
|
+
source: artifact.source || 'remote',
|
|
3618
|
+
sourceProvider: artifact.sourceProvider || 'remote',
|
|
3619
|
+
sourceScope: artifactScope,
|
|
3620
|
+
projectPath: artifactProjectPath,
|
|
3621
|
+
sourceKey,
|
|
3622
|
+
cached: true,
|
|
3623
|
+
readonly: true
|
|
3624
|
+
}, controlEntry);
|
|
2274
3625
|
}
|
|
2275
3626
|
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
cachedSkill.fullDirectory || '',
|
|
2295
|
-
cachedSkill.repoDirectory ? `${cachedSkill.repoDirectory}/${safeDirectory}` : ''
|
|
2296
|
-
]);
|
|
2297
|
-
if (detail) return detail;
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
// 缓存缺失或过期时,回退到遍历仓库配置,避免详情页报错
|
|
2301
|
-
const repos = this.loadRepos().filter(repo => repo.enabled !== false);
|
|
2302
|
-
for (const repo of repos) {
|
|
2303
|
-
const detail = await tryLoadRemoteDetailFromRepo(
|
|
2304
|
-
repo,
|
|
2305
|
-
[repo.directory ? `${repo.directory}/${safeDirectory}` : '']
|
|
2306
|
-
);
|
|
2307
|
-
if (detail) return detail;
|
|
3627
|
+
if (this.platform === 'omp') {
|
|
3628
|
+
const discovered = discoverOmpSkills(this, {
|
|
3629
|
+
...options,
|
|
3630
|
+
cwd: options.cwd || null,
|
|
3631
|
+
force: false,
|
|
3632
|
+
pathContext: this.pathContext
|
|
3633
|
+
}).find(skill => normalizeRepoPath(skill.directory) === normalizeRepoPath(safeDirectory));
|
|
3634
|
+
const discoveredPath = discovered?.realPath || discovered?.sourcePath;
|
|
3635
|
+
if (discoveredPath && fs.existsSync(discoveredPath) && !fs.lstatSync(discoveredPath).isSymbolicLink()) {
|
|
3636
|
+
return readDetail(path.dirname(discoveredPath), {
|
|
3637
|
+
...discovered,
|
|
3638
|
+
source: discovered.source || 'provider-installed',
|
|
3639
|
+
sourceProvider: discovered.sourceProvider,
|
|
3640
|
+
sourceScope: discovered.sourceScope,
|
|
3641
|
+
enabled: true,
|
|
3642
|
+
readonly: discovered.readonly !== false
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
2308
3645
|
}
|
|
2309
3646
|
|
|
2310
|
-
throw new Error('
|
|
3647
|
+
throw new Error('技能未缓存,请手动点击 Skill 面板的“刷新”后重试');
|
|
2311
3648
|
}
|
|
2312
3649
|
|
|
2313
3650
|
/**
|
|
2314
3651
|
* 获取已安装技能列表
|
|
2315
3652
|
*/
|
|
2316
|
-
getInstalledSkills() {
|
|
3653
|
+
getInstalledSkills(options = {}) {
|
|
2317
3654
|
this.refreshOmpPaths();
|
|
2318
3655
|
if (this.platform === 'omp') {
|
|
2319
|
-
return this.prepareSkills([]);
|
|
3656
|
+
return this.prepareSkills([], options);
|
|
3657
|
+
}
|
|
3658
|
+
if (options.scope === 'project') {
|
|
3659
|
+
const skills = [];
|
|
3660
|
+
this.mergeProjectSkills(skills, options);
|
|
3661
|
+
return skills;
|
|
2320
3662
|
}
|
|
2321
3663
|
const skills = [];
|
|
2322
3664
|
this.scanLocalDir(this.installDir, this.installDir, skills, {
|
|
2323
3665
|
includeHiddenDirs: this.platform === 'codex',
|
|
2324
3666
|
forceInstalled: true,
|
|
2325
|
-
source: 'native-installed'
|
|
3667
|
+
source: 'native-installed',
|
|
3668
|
+
sourceScope: 'user'
|
|
2326
3669
|
});
|
|
2327
3670
|
return skills;
|
|
2328
3671
|
}
|