MCPcopy Create free account
hub / github.com/Noumena-Network/code / getNcodeSkillScope

Function getNcodeSkillScope

src/utils/permissions/filesystem.ts:106–170  ·  view source on GitHub ↗
(
  filePath: string,
)

Source from the content-addressed store, hash-verified

104 * (settings.json, hooks/, etc.).
105 */
106export function getNcodeSkillScope(
107 filePath: string,
108): { skillName: string; pattern: string } | null {
109 const absolutePath = expandPath(filePath)
110 const absolutePathLower = normalizeCaseForComparison(absolutePath)
111
112 const bases = [
113 {
114 dir: expandPath(join(getOriginalCwd(), '.ncode', 'skills')),
115 prefix: '/.ncode/skills/',
116 },
117 {
118 dir: expandPath(join(getOriginalCwd(), '.claude', 'skills')),
119 prefix: '/.claude/skills/',
120 },
121 {
122 dir: expandPath(join(homedir(), '.ncode', 'skills')),
123 prefix: '~/.ncode/skills/',
124 },
125 {
126 dir: expandPath(join(homedir(), '.claude', 'skills')),
127 prefix: '~/.claude/skills/',
128 },
129 ]
130
131 for (const { dir, prefix } of bases) {
132 const dirLower = normalizeCaseForComparison(dir)
133 // Try both path separators (Windows paths may not be normalized to /)
134 for (const s of [sep, '/']) {
135 if (absolutePathLower.startsWith(dirLower + s.toLowerCase())) {
136 // Match on lowercase, but slice the ORIGINAL path so the skill name
137 // preserves case (pattern matching downstream is case-sensitive)
138 const rest = absolutePath.slice(dir.length + s.length)
139 const slash = rest.indexOf('/')
140 const bslash = sep === '\\' ? rest.indexOf('\\') : -1
141 const cut =
142 slash === -1
143 ? bslash
144 : bslash === -1
145 ? slash
146 : Math.min(slash, bslash)
147 // Require a separator: file must be INSIDE the skill dir, not a
148 // file directly under skills/ (no skill scope for that)
149 if (cut <= 0) return null
150 const skillName = rest.slice(0, cut)
151 // Reject traversal and empty. Use includes('..') not === '..' to
152 // match step 1.6's ruleContent.includes('..') guard: a skillName like
153 // 'v2..beta' would otherwise produce a suggestion step 1.7 emits but
154 // step 1.6 always rejects (dead suggestion, infinite re-prompt).
155 if (!skillName || skillName === '.' || skillName.includes('..')) {
156 return null
157 }
158 // Reject glob metacharacters. skillName is interpolated into a
159 // gitignore pattern consumed by ignore().add() in matchingRuleForInput
160 // at step 1.6. A directory literally named '*' (valid on POSIX) would
161 // produce '/.claude/skills/*/**' which matches ALL skills. Return null
162 // to fall through to generateSuggestions() instead.
163 if (/[*?[\]]/.test(skillName)) return null

Callers 2

Calls 3

expandPathFunction · 0.85
getOriginalCwdFunction · 0.85

Tested by

no test coverage detected