mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Summary - reuse the shared shell read parser for implicit skill doc invocation detection - add regression coverage for `nl -ba .../SKILL.md` ## Why Desktop could render `Read User Context skill` for reads recognized by the shared command parser, while implicit `skill_invocation` analytics used a separate reader allowlist and missed cases such as `nl`. ## Validation - `HOME=/private/tmp/codex-core-skills-home-pr PATH=/Users/alexsong/.cache/cargo-home/bin:$PATH CARGO_HOME=/Users/alexsong/.cache/cargo-home just test -p codex-core-skills` - `git diff --cached --check` - `just fmt` attempted; Rust formatting completed, but the Python formatters could not download uncached Ruff wheels because `files.pythonhosted.org` is blocked in this sandbox. - `bazel mod deps --lockfile_mode=update/error --repo_env=ASPECT_TOOLS_TELEMETRY= --repo_env=DO_NOT_TRACK=1` evaluated the module graph and produced no `MODULE.bazel.lock` diff, but Bazel crashed on sandboxed `sysctl` during exit.
133 lines
3.9 KiB
Rust
133 lines
3.9 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
|
|
use crate::SkillLoadOutcome;
|
|
use crate::SkillMetadata;
|
|
use codex_protocol::parse_command::ParsedCommand;
|
|
use codex_shell_command::parse_command::parse_command_impl;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
|
|
pub(crate) fn build_implicit_skill_path_indexes(
|
|
skills: Vec<SkillMetadata>,
|
|
) -> (
|
|
HashMap<AbsolutePathBuf, SkillMetadata>,
|
|
HashMap<AbsolutePathBuf, SkillMetadata>,
|
|
) {
|
|
let mut by_scripts_dir = HashMap::new();
|
|
let mut by_skill_doc_path = HashMap::new();
|
|
for skill in skills {
|
|
let skill_doc_path = canonicalize_if_exists(&skill.path_to_skills_md);
|
|
by_skill_doc_path.insert(skill_doc_path, skill.clone());
|
|
|
|
if let Some(skill_dir) = skill.path_to_skills_md.parent() {
|
|
let scripts_dir = canonicalize_if_exists(&skill_dir.join("scripts"));
|
|
by_scripts_dir.insert(scripts_dir, skill);
|
|
}
|
|
}
|
|
|
|
(by_scripts_dir, by_skill_doc_path)
|
|
}
|
|
|
|
pub fn detect_implicit_skill_invocation_for_command(
|
|
outcome: &SkillLoadOutcome,
|
|
command: &str,
|
|
workdir: &AbsolutePathBuf,
|
|
) -> Option<SkillMetadata> {
|
|
let workdir = canonicalize_if_exists(workdir);
|
|
let tokens = tokenize_command(command);
|
|
|
|
if let Some(candidate) = detect_skill_script_run(outcome, tokens.as_slice(), &workdir) {
|
|
return Some(candidate);
|
|
}
|
|
|
|
detect_skill_doc_read(outcome, tokens.as_slice(), &workdir)
|
|
}
|
|
|
|
fn tokenize_command(command: &str) -> Vec<String> {
|
|
shlex::split(command)
|
|
.unwrap_or_else(|| command.split_whitespace().map(str::to_string).collect())
|
|
}
|
|
|
|
fn script_run_token(tokens: &[String]) -> Option<&str> {
|
|
const RUNNERS: [&str; 10] = [
|
|
"python", "python3", "bash", "zsh", "sh", "node", "deno", "ruby", "perl", "pwsh",
|
|
];
|
|
const SCRIPT_EXTENSIONS: [&str; 7] = [".py", ".sh", ".js", ".ts", ".rb", ".pl", ".ps1"];
|
|
|
|
let runner_token = tokens.first()?;
|
|
let runner = command_basename(runner_token).to_ascii_lowercase();
|
|
let runner = runner.strip_suffix(".exe").unwrap_or(&runner);
|
|
if !RUNNERS.contains(&runner) {
|
|
return None;
|
|
}
|
|
|
|
let mut script_token = None;
|
|
for token in tokens.iter().skip(1) {
|
|
if token == "--" || token.starts_with('-') {
|
|
continue;
|
|
}
|
|
script_token = Some(token.as_str());
|
|
break;
|
|
}
|
|
let script_token = script_token?;
|
|
if SCRIPT_EXTENSIONS
|
|
.iter()
|
|
.any(|extension| script_token.to_ascii_lowercase().ends_with(extension))
|
|
{
|
|
return Some(script_token);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn detect_skill_script_run(
|
|
outcome: &SkillLoadOutcome,
|
|
tokens: &[String],
|
|
workdir: &AbsolutePathBuf,
|
|
) -> Option<SkillMetadata> {
|
|
let script_token = script_run_token(tokens)?;
|
|
let script_path = Path::new(script_token);
|
|
let script_path = canonicalize_if_exists(&workdir.join(script_path));
|
|
|
|
for path in script_path.ancestors() {
|
|
if let Some(candidate) = outcome.implicit_skills_by_scripts_dir.get(&path) {
|
|
return Some(candidate.clone());
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn detect_skill_doc_read(
|
|
outcome: &SkillLoadOutcome,
|
|
tokens: &[String],
|
|
workdir: &AbsolutePathBuf,
|
|
) -> Option<SkillMetadata> {
|
|
for command in parse_command_impl(tokens) {
|
|
if let ParsedCommand::Read { path, .. } = command {
|
|
let candidate_path = canonicalize_if_exists(&workdir.join(path.as_path()));
|
|
if let Some(candidate) = outcome.implicit_skills_by_doc_path.get(&candidate_path) {
|
|
return Some(candidate.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn command_basename(command: &str) -> String {
|
|
Path::new(command)
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.unwrap_or(command)
|
|
.to_string()
|
|
}
|
|
|
|
fn canonicalize_if_exists(path: &AbsolutePathBuf) -> AbsolutePathBuf {
|
|
path.canonicalize().unwrap_or_else(|_| path.clone())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "invocation_utils_tests.rs"]
|
|
mod tests;
|