{"record":{"id":"6ce5b3e08cd3a4ab","repo":"can1357/oh-my-pi","slug":"skill-url-requires-a-skill-name-url","errorCode":null,"errorMessage":"skill:// URL requires a skill name: ${url}","messagePattern":"skill:// URL requires a skill name: (.+?)","errorType":"validation","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/bash-skill-urls.ts","lineNumber":61,"sourceCode":"\tlocalOptions?: LocalProtocolOptions;\n\tcwd?: string;\n\tsessionFile?: string;\n\tensureLocalParentDirs?: boolean;\n}\n\n/**\n * Resolve a single skill:// URL to its absolute filesystem path.\n * Does NOT read file content or verify existence.\n */\nexport function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): string {\n\tconst parsed = /^skill:\\/\\/([^/?#]+)(\\/[^?#]*)?(?:[?#].*)?$/.exec(url);\n\tif (!parsed) {\n\t\tthrow new ToolError(`Invalid skill:// URL: ${url}`);\n\t}\n\n\tlet rawSkillSegment = parsed[1];\n\tif (!rawSkillSegment) {\n\t\tthrow new ToolError(`skill:// URL requires a skill name: ${url}`);\n\t}\n\t// Decode percent-encoded colons (%3A) used for namespaced skill names\n\ttry {\n\t\trawSkillSegment = decodeURIComponent(rawSkillSegment);\n\t} catch {\n\t\t// Leave as-is if decoding fails\n\t}\n\n\t// Resolve skill name by longest-prefix match against registered skills.\n\t// This handles namespaced skills (\"plugin:skill\") where the URI may also\n\t// carry a colon-delimited suffix (e.g., \":1-5\" line range).\n\tconst { skill, suffix } = matchSkillName(rawSkillSegment, skills);\n\tif (!skill) {\n\t\tconst available = skills.map(s => s.name);\n\t\tconst availableStr = available.length > 0 ? available.join(\", \") : \"none\";\n\t\tthrow new ToolError(`Unknown skill: ${rawSkillSegment}. Available: ${availableStr}`);\n\t}\n","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/bash-skill-urls.ts#L43-L79","documentation":"Although the regex matched, the captured skill-name segment was empty. The function requires at least a skill name after skill:// and throws when it is absent. In practice this guards a defensive branch on the URL grammar.","triggerScenarios":"A URL like \"skill://\" where the regex's ([^/?#]+) matched via an alternate path producing an empty first segment — practically seen when callers build the URL by concatenating an empty skill-name variable.","commonSituations":"Template/codegen producing `skill://${name}/...` where `name` is empty; an upstream parser stripping the name as a namespace prefix.","solutions":["Include a valid skill name in the URL: skill://my-skill/path.","Verify the skill-name variable is non-empty before interpolating it into the URL.","List available skills first (see the skills registry) and pick an existing name."],"exampleFix":"// before\nconst url = `skill://${skillName ?? \"\"}/SKILL.md`;\n\n// after\nif (!skillName) throw new Error(\"skill name required\");\nconst url = `skill://${skillName}/SKILL.md`;","handlingStrategy":"validation","validationCode":"const m = /^skill:\\/\\/([^/?#]+)/.exec(url);\nif (!m || !m[1]) throw new Error(`skill URL missing name: ${url}`);","typeGuard":"function hasSkillName(url: string): boolean {\n  const m = /^skill:\\/\\/([^/?#]+)/.exec(url);\n  return !!m && m[1].length > 0;\n}","tryCatchPattern":"try {\n  return resolveSkillUrlToPath(url, skills);\n} catch (e) {\n  if (e instanceof ToolError && e.message.includes(\"requires a skill name\")) {\n    // reconstruct the URL with an explicit skill name\n  } else throw e;\n}","preventionTips":["Never interpolate an optional variable directly into the skill name slot.","Validate non-empty name at URL-construction time."],"tags":["url-parsing","validation","skills"],"backgroundTag":"missing-required-segment","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}