@zibby/skills 2.0.20 → 2.0.22

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.
@@ -35,12 +35,22 @@
35
35
  * `semgrep` CLI, no network, no telemetry, no registry) with a VENDORED curated
36
36
  * ruleset + a generated local targets file; see resolveSemgrepBin. It scans ALL
37
37
  * its languages in ONE invocation via a `-targets` file. JS/TS is intentionally
38
- * left to oxlint (semgrep EXCLUDES it) to avoid double-scanning.
38
+ * left to oxlint (semgrep EXCLUDES it) to avoid double-scanning. Its 263 MB
39
+ * engine is the one thing here that is NOT in the image: it is materialized ON
40
+ * DEMAND from a sha256-pinned artifact on our own CDN the first time a scan
41
+ * needs it (see resolveSemgrepBin), so image size stops tracking how many
42
+ * engines the product supports.
39
43
  * ruff (Python) + staticcheck (Go) remain SCAFFOLD entries (registry + parser
40
44
  * present, clearly marked TODO) — semgrep now covers Python/Go for BREADTH; ruff/
41
45
  * staticcheck can still be wired later for DEPTH. Best-effort throughout: a missing
42
46
  * binary (spawn ENOENT), an unreadable file, or a parser hiccup NEVER throws — the
43
47
  * scanner is skipped with a note and the others still run.
48
+ *
49
+ * ONE THING IS NOT BEST-EFFORT: an engine whose DELIVERY fails (download broke,
50
+ * sha256 mismatch, archive won't unpack). That is not "this stack has no linter",
51
+ * it is "the analysis you asked for silently did not happen", so it surfaces as
52
+ * `unavailable` on the scanner block plus a top-level `degraded` array — never as
53
+ * a skip. See runScanner.
44
54
  */
45
55
  /**
46
56
  * Build the semgrep-core `-targets` document for a set of RELATIVE file paths. This
@@ -78,12 +88,19 @@ export declare function parseOxlint(stdout: any): any;
78
88
  * parse (stdout, stderr, code) => [{ file, line, severity, rule, message }]
79
89
  * Adding a tool = ONE more entry here. NOTHING scanner-specific lives elsewhere.
80
90
  */
81
- export declare const SCANNERS: {
91
+ export declare const SCANNERS: ({
82
92
  id: string;
83
93
  detect: (dir: any) => boolean;
84
94
  langs: string[];
85
95
  bin: () => string;
86
96
  args: (files: any, ctx?: any) => any[];
87
97
  parse: typeof parseOxlint;
88
- }[];
98
+ } | {
99
+ id: string;
100
+ detect: (dir: any) => boolean;
101
+ langs: string[];
102
+ bin: () => Promise<any>;
103
+ args: (files: any, ctx?: any) => any[];
104
+ parse: typeof parseSemgrep;
105
+ })[];
89
106
  export declare const codeScanSkill: any;
package/dist/code-scan.js CHANGED
@@ -1,9 +1,12 @@
1
- import{spawnSync as O}from"node:child_process";import{existsSync as u,readdirSync as _,statSync as w,writeFileSync as S,mkdirSync as b}from"node:fs";import{dirname as h,extname as m,join as l,relative as T,resolve as d}from"node:path";import{tmpdir as v}from"node:os";import{fileURLToPath as P}from"node:url";import{SKILL_META as C}from"@zibby/skill-ids";import{binPath as I}from"@zibby/bin-oxlint";import{binPath as F}from"@zibby/bin-semgrep";function L(){if(process.env.OXLINT_BIN)return process.env.OXLINT_BIN;try{let e=I();if(e&&u(e))return e}catch{}return"oxlint"}function B(){if(process.env.SEMGREP_CORE_BIN)return process.env.SEMGREP_CORE_BIN;try{let e=F();if(e&&u(e))return e}catch{}return"semgrep-core"}function D(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let e=h(P(import.meta.url)),n=d(e,"..","bin","mcp-skill.mjs");return u(n)?n:null}var E=new Set(["node_modules",".git","dist","build","out","vendor","target",".venv","venv","__pycache__",".next",".turbo","coverage",".zibby"]),G=400,J={plugins:["react","typescript","unicorn","oxc"],categories:{correctness:"error",suspicious:"warn"},rules:{"react/react-in-jsx-scope":"off","react/jsx-max-depth":"off","react/no-array-index-key":"off","react/jsx-key":"error","react/no-unknown-property":"error","no-unused-vars":"warn",eqeqeq:"warn"}},M=[".oxlintrc.json",".oxlintrc","oxlint.json"],p=null;function z(){if(p&&u(p))return p;try{let e=l(v(),"zibby-code-scan");b(e,{recursive:!0});let n=l(e,"oxlintrc.curated.json");return S(n,JSON.stringify(J),"utf-8"),p=n,n}catch{return null}}var N={".java":"java",".py":"python",".go":"go",".rb":"ruby",".php":"php"},x=Object.keys(N),j={rules:[{id:"zibby-java-command-injection",languages:["java"],severity:"ERROR",message:"Command execution (Runtime.exec / ProcessBuilder) \u2014 command injection risk if the argument is attacker-influenced. Validate/allow-list the input or avoid a shell.",patterns:[{"pattern-either":[{pattern:"Runtime.getRuntime().exec(...)"},{pattern:"new ProcessBuilder(...)"}]}]},{id:"zibby-python-subprocess-shell",languages:["python"],severity:"ERROR",message:"subprocess call with shell=True \u2014 command injection risk. Pass an argv list and shell=False.",pattern:"subprocess.$F(..., shell=True, ...)"},{id:"zibby-python-yaml-load",languages:["python"],severity:"WARNING",message:"yaml.load without a safe loader can instantiate arbitrary Python objects. Use yaml.safe_load.",pattern:"yaml.load(...)"},{id:"zibby-go-command-injection",languages:["go"],severity:"WARNING",message:"os/exec with a non-constant command \u2014 verify the value is not attacker-controlled (command injection).",pattern:"exec.Command($CMD, ...)"},{id:"zibby-ruby-command-injection",languages:["ruby"],severity:"ERROR",message:"Shell/eval execution (system / eval) \u2014 command injection risk if the argument is attacker-influenced.",patterns:[{"pattern-either":[{pattern:"system(...)"},{pattern:"eval(...)"}]}]},{id:"zibby-php-command-injection",languages:["php"],severity:"ERROR",message:"Shell/eval execution (system / exec / shell_exec) \u2014 command injection risk if the argument is attacker-influenced.",patterns:[{"pattern-either":[{pattern:"system(...);"},{pattern:"exec(...);"},{pattern:"shell_exec(...);"}]}]}]},H=[".semgrep.yml",".semgrep.yaml","semgrep.yml","semgrep.yaml"],f=null;function U(){if(f&&u(f))return f;try{let e=l(v(),"zibby-code-scan");b(e,{recursive:!0});let n=l(e,"semgrep.curated.rules.json");return S(n,JSON.stringify(j),"utf-8"),f=n,n}catch{return null}}function $(e){return H.map(s=>l(e,s)).find(s=>u(s))||U()}function W(e){let n=[];for(let s of Array.isArray(e)?e:[]){if(typeof s!="string"||!s)continue;let t=N[m(s).toLowerCase()];if(!t)continue;let i=s.replace(/\\/g,"/");n.push(["CodeTarget",{path:{fpath:i,ppath:`/${i.replace(/^\/+/,"")}`},analyzer:t,products:["sast"]}])}return["Targets",n]}var X=0;function q(e){let n=W(e),s=n[1].length,t=l(v(),"zibby-code-scan");b(t,{recursive:!0});let i=l(t,`semgrep.targets.${process.pid}.${X++}.json`);return S(i,JSON.stringify(n),"utf-8"),{path:i,count:s}}function R(e){let n=typeof e=="string"?e.toUpperCase():"";return n==="ERROR"?"error":n==="INFO"||n==="INVENTORY"||n==="EXPERIMENT"?"info":"warning"}var K=Object.fromEntries(j.rules.map(e=>[e.id,e.severity]));function V(e,n){if(n)return R(n);let s=K[e];return s?R(s):"warning"}function Y(e){let n=String(e||""),s=n.indexOf("{");if(s<0)return[];let t;try{t=JSON.parse(n.slice(s))}catch{return[]}return(t&&Array.isArray(t.results)?t.results:[]).map(r=>{if(!r||typeof r!="object")return null;let o=r.start&&typeof r.start=="object"?r.start:{},a=r.extra&&typeof r.extra=="object"?r.extra:{};return{file:r.path||"",line:Number.isFinite(o.line)?o.line:"",severity:V(r.check_id,a.severity),rule:r.check_id||"",message:(a.message||"").trim()}}).filter(r=>r&&(r.file||r.message))}function Q(e,n,s=4e3){let t=new Set(n.map(o=>o.toLowerCase())),i=[e],r=0;for(;i.length;){let o=i.pop(),a;try{a=_(o,{withFileTypes:!0})}catch{continue}for(let c of a){if(++r>s)return!1;if(c.isDirectory())!E.has(c.name)&&!c.name.startsWith(".")&&i.push(l(o,c.name));else if(c.isFile()&&t.has(m(c.name).toLowerCase()))return!0}}return!1}function Z(e){let n=String(e||"").trim();if(!n)return[];let s;try{s=JSON.parse(n)}catch{return[]}return(Array.isArray(s)?s:s&&Array.isArray(s.diagnostics)?s.diagnostics:[]).map(i=>{if(!i||typeof i!="object")return null;let r=Array.isArray(i.labels)&&i.labels.length?i.labels[0]:null,o=r&&r.span?r.span:null;return{file:i.filename||o&&o.filename||"",line:o&&Number.isFinite(o.line)?o.line:"",severity:i.severity||"warning",rule:i.code||"",message:i.message||""}}).filter(i=>i&&(i.file||i.message))}function ee(e){let n=String(e||"").trim();if(!n)return[];let s;try{s=JSON.parse(n)}catch{return[]}return Array.isArray(s)?s.map(t=>t&&typeof t=="object"?{file:t.filename||"",line:t.location&&Number.isFinite(t.location.row)?t.location.row:"",severity:"warning",rule:t.code||"",message:t.message||""}:null).filter(t=>t&&(t.file||t.message)):[]}function te(e){let n=String(e||"").trim();if(!n)return[];let s=[];for(let t of n.split(`
2
- `)){let i=t.trim();if(!i)continue;let r;try{r=JSON.parse(i)}catch{continue}if(!r||typeof r!="object")continue;let o=r.location||{};s.push({file:o.file||"",line:Number.isFinite(o.line)?o.line:"",severity:r.severity||"warning",rule:r.code||"",message:r.message||""})}return s.filter(t=>t.file||t.message)}var ne=[{id:"oxlint",detect:e=>u(l(e,"package.json")),langs:[".ts",".tsx",".js",".jsx",".mjs",".cjs"],bin:()=>L(),args:(e,n={})=>{let s=n.baseDir||".",i=M.some(o=>u(l(s,o)))?null:z();return["--format","json",...i?["--config",i]:[],...e]},parse:Z},{id:"semgrep",detect:e=>Q(e,x),langs:x,bin:()=>B(),args:(e,n={})=>{let s=n.baseDir||".",t=$(s),{path:i}=q(e);return[...t?["-rules",t]:[],"-targets",i,"-json"]},parse:Y},{id:"ruff",detect:e=>u(l(e,"pyproject.toml"))||u(l(e,"requirements.txt"))||u(l(e,"setup.py")),langs:[".py"],bin:()=>process.env.RUFF_BIN||"ruff",args:e=>["check","--output-format","json",...e],parse:ee},{id:"staticcheck",detect:e=>u(l(e,"go.mod")),langs:[".go"],bin:()=>process.env.STATICCHECK_BIN||"staticcheck",args:e=>["-f","json",...e],parse:te}];function re(e,n,s){let t=[],i=new Set(n.map(o=>o.toLowerCase())),r=[e];for(;r.length&&t.length<s;){let o=r.pop(),a;try{a=_(o,{withFileTypes:!0})}catch{continue}for(let c of a){if(t.length>=s)break;c.isDirectory()?!E.has(c.name)&&!c.name.startsWith(".")&&r.push(l(o,c.name)):c.isFile()&&i.has(m(c.name).toLowerCase())&&t.push(l(o,c.name))}}return t}function se(e,n,s){let t=s.map(a=>T(n,a)).filter(Boolean);if(!t.length)return{scanner:e.id,skipped:"no matching files"};let i=e.bin(),r=O(i,e.args(t,{baseDir:n}),{cwd:n,encoding:"utf-8",timeout:180*1e3,maxBuffer:32*1024*1024});if(r.error){let a=r.error.code==="ENOENT"?`binary not installed (${i})`:String(r.error.message||r.error);return{scanner:e.id,skipped:a}}let o=[];try{let a=e.parse(r.stdout,r.stderr,r.status);o=Array.isArray(a)?a.filter(Boolean):[]}catch{o=[]}return{scanner:e.id,filesScanned:t.length,findings:o}}var me={id:"code-scan",serverName:"code_scan",meta:C["code-scan"],allowedTools:["mcp__code_scan__*"],description:"Code scan \u2014 run the RIGHT deterministic linter/analyzer for a checked-out repo (auto-detects the stack: JS/TS\u2192oxlint; Java/Python/Go/Ruby/PHP\u2192semgrep) and return structured findings. Fully local; the code never leaves the box.",promptFragment:`## Code Scan (deterministic linter, auto-detects the stack)
1
+ import{spawnSync as A}from"node:child_process";import{existsSync as u,readdirSync as _,statSync as N,writeFileSync as v,mkdirSync as b}from"node:fs";import{dirname as S,extname as p,join as l,relative as P,resolve as g}from"node:path";import{tmpdir as w}from"node:os";import{fileURLToPath as I}from"node:url";import{SKILL_META as C}from"@zibby/skill-ids";import{binPath as F}from"@zibby/bin-oxlint";import{ensureBinPath as L}from"@zibby/bin-semgrep";function D(){if(process.env.OXLINT_BIN)return process.env.OXLINT_BIN;try{let e=F();if(e&&u(e))return e}catch{}return"oxlint"}async function B(){return process.env.SEMGREP_CORE_BIN?process.env.SEMGREP_CORE_BIN:L()}function J(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let e=S(I(import.meta.url)),n=g(e,"..","bin","mcp-skill.mjs");return u(n)?n:null}var E=new Set(["node_modules",".git","dist","build","out","vendor","target",".venv","venv","__pycache__",".next",".turbo","coverage",".zibby"]),G=400,$={plugins:["react","typescript","unicorn","oxc"],categories:{correctness:"error",suspicious:"warn"},rules:{"react/react-in-jsx-scope":"off","react/jsx-max-depth":"off","react/no-array-index-key":"off","react/jsx-key":"error","react/no-unknown-property":"error","no-unused-vars":"warn",eqeqeq:"warn"}},M=[".oxlintrc.json",".oxlintrc","oxlint.json"],d=null;function z(){if(d&&u(d))return d;try{let e=l(w(),"zibby-code-scan");b(e,{recursive:!0});let n=l(e,"oxlintrc.curated.json");return v(n,JSON.stringify($),"utf-8"),d=n,n}catch{return null}}var k={".java":"java",".py":"python",".go":"go",".rb":"ruby",".php":"php"},x=Object.keys(k),j={rules:[{id:"zibby-java-command-injection",languages:["java"],severity:"ERROR",message:"Command execution (Runtime.exec / ProcessBuilder) \u2014 command injection risk if the argument is attacker-influenced. Validate/allow-list the input or avoid a shell.",patterns:[{"pattern-either":[{pattern:"Runtime.getRuntime().exec(...)"},{pattern:"new ProcessBuilder(...)"}]}]},{id:"zibby-python-subprocess-shell",languages:["python"],severity:"ERROR",message:"subprocess call with shell=True \u2014 command injection risk. Pass an argv list and shell=False.",pattern:"subprocess.$F(..., shell=True, ...)"},{id:"zibby-python-yaml-load",languages:["python"],severity:"WARNING",message:"yaml.load without a safe loader can instantiate arbitrary Python objects. Use yaml.safe_load.",pattern:"yaml.load(...)"},{id:"zibby-go-command-injection",languages:["go"],severity:"WARNING",message:"os/exec with a non-constant command \u2014 verify the value is not attacker-controlled (command injection).",pattern:"exec.Command($CMD, ...)"},{id:"zibby-ruby-command-injection",languages:["ruby"],severity:"ERROR",message:"Shell/eval execution (system / eval) \u2014 command injection risk if the argument is attacker-influenced.",patterns:[{"pattern-either":[{pattern:"system(...)"},{pattern:"eval(...)"}]}]},{id:"zibby-php-command-injection",languages:["php"],severity:"ERROR",message:"Shell/eval execution (system / exec / shell_exec) \u2014 command injection risk if the argument is attacker-influenced.",patterns:[{"pattern-either":[{pattern:"system(...);"},{pattern:"exec(...);"},{pattern:"shell_exec(...);"}]}]}]},H=[".semgrep.yml",".semgrep.yaml","semgrep.yml","semgrep.yaml"],f=null;function U(){if(f&&u(f))return f;try{let e=l(w(),"zibby-code-scan");b(e,{recursive:!0});let n=l(e,"semgrep.curated.rules.json");return v(n,JSON.stringify(j),"utf-8"),f=n,n}catch{return null}}function W(e){return H.map(s=>l(e,s)).find(s=>u(s))||U()}function X(e){let n=[];for(let s of Array.isArray(e)?e:[]){if(typeof s!="string"||!s)continue;let t=k[p(s).toLowerCase()];if(!t)continue;let i=s.replace(/\\/g,"/");n.push(["CodeTarget",{path:{fpath:i,ppath:`/${i.replace(/^\/+/,"")}`},analyzer:t,products:["sast"]}])}return["Targets",n]}var q=0;function K(e){let n=X(e),s=n[1].length,t=l(w(),"zibby-code-scan");b(t,{recursive:!0});let i=l(t,`semgrep.targets.${process.pid}.${q++}.json`);return v(i,JSON.stringify(n),"utf-8"),{path:i,count:s}}function R(e){let n=typeof e=="string"?e.toUpperCase():"";return n==="ERROR"?"error":n==="INFO"||n==="INVENTORY"||n==="EXPERIMENT"?"info":"warning"}var V=Object.fromEntries(j.rules.map(e=>[e.id,e.severity]));function Y(e,n){if(n)return R(n);let s=V[e];return s?R(s):"warning"}function Q(e){let n=String(e||""),s=n.indexOf("{");if(s<0)return[];let t;try{t=JSON.parse(n.slice(s))}catch{return[]}return(t&&Array.isArray(t.results)?t.results:[]).map(r=>{if(!r||typeof r!="object")return null;let a=r.start&&typeof r.start=="object"?r.start:{},c=r.extra&&typeof r.extra=="object"?r.extra:{};return{file:r.path||"",line:Number.isFinite(a.line)?a.line:"",severity:Y(r.check_id,c.severity),rule:r.check_id||"",message:(c.message||"").trim()}}).filter(r=>r&&(r.file||r.message))}function Z(e,n,s=4e3){let t=new Set(n.map(a=>a.toLowerCase())),i=[e],r=0;for(;i.length;){let a=i.pop(),c;try{c=_(a,{withFileTypes:!0})}catch{continue}for(let o of c){if(++r>s)return!1;if(o.isDirectory())!E.has(o.name)&&!o.name.startsWith(".")&&i.push(l(a,o.name));else if(o.isFile()&&t.has(p(o.name).toLowerCase()))return!0}}return!1}function ee(e){let n=String(e||"").trim();if(!n)return[];let s;try{s=JSON.parse(n)}catch{return[]}return(Array.isArray(s)?s:s&&Array.isArray(s.diagnostics)?s.diagnostics:[]).map(i=>{if(!i||typeof i!="object")return null;let r=Array.isArray(i.labels)&&i.labels.length?i.labels[0]:null,a=r&&r.span?r.span:null;return{file:i.filename||a&&a.filename||"",line:a&&Number.isFinite(a.line)?a.line:"",severity:i.severity||"warning",rule:i.code||"",message:i.message||""}}).filter(i=>i&&(i.file||i.message))}function te(e){let n=String(e||"").trim();if(!n)return[];let s;try{s=JSON.parse(n)}catch{return[]}return Array.isArray(s)?s.map(t=>t&&typeof t=="object"?{file:t.filename||"",line:t.location&&Number.isFinite(t.location.row)?t.location.row:"",severity:"warning",rule:t.code||"",message:t.message||""}:null).filter(t=>t&&(t.file||t.message)):[]}function ne(e){let n=String(e||"").trim();if(!n)return[];let s=[];for(let t of n.split(`
2
+ `)){let i=t.trim();if(!i)continue;let r;try{r=JSON.parse(i)}catch{continue}if(!r||typeof r!="object")continue;let a=r.location||{};s.push({file:a.file||"",line:Number.isFinite(a.line)?a.line:"",severity:r.severity||"warning",rule:r.code||"",message:r.message||""})}return s.filter(t=>t.file||t.message)}var re=[{id:"oxlint",detect:e=>u(l(e,"package.json")),langs:[".ts",".tsx",".js",".jsx",".mjs",".cjs"],bin:()=>D(),args:(e,n={})=>{let s=n.baseDir||".",i=M.some(a=>u(l(s,a)))?null:z();return["--format","json",...i?["--config",i]:[],...e]},parse:ee},{id:"semgrep",detect:e=>Z(e,x),langs:x,bin:()=>B(),args:(e,n={})=>{let s=n.baseDir||".",t=W(s),{path:i}=K(e);return[...t?["-rules",t]:[],"-targets",i,"-json"]},parse:Q},{id:"ruff",detect:e=>u(l(e,"pyproject.toml"))||u(l(e,"requirements.txt"))||u(l(e,"setup.py")),langs:[".py"],bin:()=>process.env.RUFF_BIN||"ruff",args:e=>["check","--output-format","json",...e],parse:te},{id:"staticcheck",detect:e=>u(l(e,"go.mod")),langs:[".go"],bin:()=>process.env.STATICCHECK_BIN||"staticcheck",args:e=>["-f","json",...e],parse:ne}];function se(e,n,s){let t=[],i=new Set(n.map(a=>a.toLowerCase())),r=[e];for(;r.length&&t.length<s;){let a=r.pop(),c;try{c=_(a,{withFileTypes:!0})}catch{continue}for(let o of c){if(t.length>=s)break;o.isDirectory()?!E.has(o.name)&&!o.name.startsWith(".")&&r.push(l(a,o.name)):o.isFile()&&i.has(p(o.name).toLowerCase())&&t.push(l(a,o.name))}}return t}async function ie(e,n,s){let t=s.map(c=>P(n,c)).filter(Boolean);if(!t.length)return{scanner:e.id,skipped:"no matching files"};let i;try{i=await e.bin()}catch(c){return c?.isDeliveryFailure?{scanner:e.id,unavailable:`${e.id} unavailable: ${c.reason==="download-failed"?"download failed":c.reason}`,reason:c.reason,detail:String(c.message||c),impact:`${t.length} ${[...new Set(t.map(o=>p(o).toLowerCase()))].sort().join("/")} file(s) were NOT statically analysed. Treat this review as INCOMPLETE for those files and say so.`}:{scanner:e.id,skipped:`binary not available (${c?.reason||"unknown"}): ${String(c?.message||c)}`}}let r=A(i,e.args(t,{baseDir:n}),{cwd:n,encoding:"utf-8",timeout:180*1e3,maxBuffer:32*1024*1024});if(r.error){let c=r.error.code==="ENOENT"?`binary not installed (${i})`:String(r.error.message||r.error);return{scanner:e.id,skipped:c}}let a=[];try{let c=e.parse(r.stdout,r.stderr,r.status);a=Array.isArray(c)?c.filter(Boolean):[]}catch{a=[]}return{scanner:e.id,filesScanned:t.length,findings:a}}var me={id:"code-scan",serverName:"code_scan",meta:C["code-scan"],allowedTools:["mcp__code_scan__*"],description:"Code scan \u2014 run the RIGHT deterministic linter/analyzer for a checked-out repo (auto-detects the stack: JS/TS\u2192oxlint; Java/Python/Go/Ruby/PHP\u2192semgrep) and return structured findings. Fully local; the code never leaves the box.",promptFragment:`## Code Scan (deterministic linter, auto-detects the stack)
3
3
  After you've cloned the repo, call \`scan_code\` to get DETERMINISTIC linter
4
4
  findings for WHATEVER stack this repo is \u2014 it auto-detects (JS/TS\u2192oxlint;
5
5
  Java/Python/Go/Ruby/PHP\u2192semgrep) and runs the matching tool. Pass \`files\` (the changed files, ideal for
6
6
  a review) or \`dir\` (a directory to scan). Findings are GROUND-TRUTH CANDIDATES:
7
7
  triage them for THIS change, verify each in context (false positives exist \u2014
8
8
  trace before asserting), fold noise, and turn the real ones into inline
9
- suggestions. Don't hand-lint what the tool already covers, and don't re-run it.`,resolve(){let e=D();return e?{type:"stdio",command:"node",args:[e,"../dist/code-scan.js","codeScanSkill"],env:{},description:this.description}:{command:null,args:[],env:{},description:this.description}},async handleToolCall(e,n){if(e!=="scan_code")return JSON.stringify({error:`Unknown tool: ${e}`});try{let s=Array.isArray(n?.files)?n.files.filter(a=>typeof a=="string"&&a.trim()):null,t;if(n?.dir&&typeof n.dir=="string"?t=d(n.dir):s&&s.length?t=ie(s.map(a=>d(a))):t=process.cwd(),!u(t)||!w(t).isDirectory())return JSON.stringify({error:`dir does not exist or is not a directory: ${t}`});let i=s?s.map(a=>d(t,a)):null,r=[],o=0;for(let a of ne){let c=!1;try{c=!!a.detect(t)}catch{c=!1}if(!c)continue;let k=new Set(a.langs.map(y=>y.toLowerCase())),A=i?i.filter(y=>k.has(m(y).toLowerCase())):re(t,a.langs,G),g=se(a,t,A);Array.isArray(g.findings)&&(o+=g.findings.length),r.push(g)}return r.length?JSON.stringify({ok:!0,baseDir:t,totalFindings:o,scanners:r}):JSON.stringify({ok:!0,baseDir:t,scanners:[],totalFindings:0,note:"No known stack detected (no package.json / pyproject / go.mod \u2026). Review by hand."})}catch(s){return JSON.stringify({error:`scan_code failed: ${s.message}`})}},tools:[{name:"scan_code",description:"Run the right deterministic linter for a checked-out repo and return structured findings. Auto-detects the stack (JS/TS \u2192 oxlint; Java/Python/Go/Ruby/PHP \u2192 semgrep OSS) and runs each matching tool, scoped to files in its languages. Pass `files` (e.g. the changed files of the PR \u2014 recommended for a review) OR `dir` (a directory to scan). Returns { scanners: [ { scanner, findings: [ { file, line, severity, rule, message } ] } ] }. Findings are CANDIDATES \u2014 verify each in context before asserting. Best-effort: a stack whose linter is not installed is skipped with a note.",input_schema:{type:"object",properties:{dir:{type:"string",description:"Absolute path to the checked-out repo (or subdirectory) to scan. Defaults to the current working directory."},files:{type:"array",items:{type:"string"},description:"Explicit list of files to scan (paths relative to `dir`, or absolute). Best for a code review \u2014 pass the PR's changed files. When omitted, the whole `dir` is walked (bounded)."}}}}]};function ie(e){if(!e.length)return process.cwd();if(e.length===1)return h(e[0]);let n=e.map(r=>r.split("/")),s=n[0],t=[];for(let r=0;r<s.length;r++){let o=s[r];if(n.every(a=>a[r]===o))t.push(o);else break}let i=t.join("/");return i&&u(i)&&w(i).isDirectory()?i:h(e[0])}export{ne as SCANNERS,W as buildSemgrepTargets,me as codeScanSkill,Z as parseOxlint,Y as parseSemgrep};
9
+ suggestions. Don't hand-lint what the tool already covers, and don't re-run it.
10
+ If the result has a \`degraded\` array, an engine failed to DOWNLOAD \u2014 those files
11
+ were not scanned at all. Say so in your review; never let a delivery failure read
12
+ as a clean bill of health.`,resolve(){let e=J();return e?{type:"stdio",command:"node",args:[e,"../dist/code-scan.js","codeScanSkill"],env:{},description:this.description}:{command:null,args:[],env:{},description:this.description}},async handleToolCall(e,n){if(e!=="scan_code")return JSON.stringify({error:`Unknown tool: ${e}`});try{let s=Array.isArray(n?.files)?n.files.filter(o=>typeof o=="string"&&o.trim()):null,t;if(n?.dir&&typeof n.dir=="string"?t=g(n.dir):s&&s.length?t=ae(s.map(o=>g(o))):t=process.cwd(),!u(t)||!N(t).isDirectory())return JSON.stringify({error:`dir does not exist or is not a directory: ${t}`});let i=s?s.map(o=>g(t,o)):null,r=[],a=0;for(let o of re){let m=!1;try{m=!!o.detect(t)}catch{m=!1}if(!m)continue;let O=new Set(o.langs.map(h=>h.toLowerCase())),T=i?i.filter(h=>O.has(p(h).toLowerCase())):se(t,o.langs,G),y=await ie(o,t,T);Array.isArray(y.findings)&&(a+=y.findings.length),r.push(y)}if(!r.length)return JSON.stringify({ok:!0,baseDir:t,scanners:[],totalFindings:0,note:"No known stack detected (no package.json / pyproject / go.mod \u2026). Review by hand."});let c=r.filter(o=>o.unavailable).map(o=>`${o.unavailable} \u2014 ${o.impact}`);return c.length?JSON.stringify({ok:!0,baseDir:t,totalFindings:a,degraded:c,scanners:r}):JSON.stringify({ok:!0,baseDir:t,totalFindings:a,scanners:r})}catch(s){return JSON.stringify({error:`scan_code failed: ${s.message}`})}},tools:[{name:"scan_code",description:"Run the right deterministic linter for a checked-out repo and return structured findings. Auto-detects the stack (JS/TS \u2192 oxlint; Java/Python/Go/Ruby/PHP \u2192 semgrep OSS) and runs each matching tool, scoped to files in its languages. Pass `files` (e.g. the changed files of the PR \u2014 recommended for a review) OR `dir` (a directory to scan). Returns { scanners: [ { scanner, findings: [ { file, line, severity, rule, message } ] } ] }. Findings are CANDIDATES \u2014 verify each in context before asserting. Best-effort: a stack whose linter is not installed is skipped with a note. If the result carries a `degraded` array, an engine that SHOULD have run could not be delivered \u2014 those languages were not analysed at all, so say the review is incomplete for them rather than implying they came back clean.",input_schema:{type:"object",properties:{dir:{type:"string",description:"Absolute path to the checked-out repo (or subdirectory) to scan. Defaults to the current working directory."},files:{type:"array",items:{type:"string"},description:"Explicit list of files to scan (paths relative to `dir`, or absolute). Best for a code review \u2014 pass the PR's changed files. When omitted, the whole `dir` is walked (bounded)."}}}}]};function ae(e){if(!e.length)return process.cwd();if(e.length===1)return S(e[0]);let n=e.map(r=>r.split("/")),s=n[0],t=[];for(let r=0;r<s.length;r++){let a=s[r];if(n.every(c=>c[r]===a))t.push(a);else break}let i=t.join("/");return i&&u(i)&&N(i).isDirectory()?i:S(e[0])}export{re as SCANNERS,X as buildSemgrepTargets,me as codeScanSkill,ee as parseOxlint,Q as parseSemgrep};