pkgxray 0.1.0 → 0.2.0

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/bin/audit.js CHANGED
@@ -5,6 +5,7 @@ const fs = require("node:fs");
5
5
  const { auditEvidence, renderMarkdown } = require("../src/auditor");
6
6
  const { guardExtension } = require("../src/quarantine");
7
7
  const { reasonAbout } = require("../src/reasoner");
8
+ const { detectAvailableProvider, reasoningSetupHint } = require("../src/providers");
8
9
 
9
10
  function printUsage() {
10
11
  process.stderr.write(
@@ -19,11 +20,11 @@ function printUsage() {
19
20
  "Evidence JSON fields:",
20
21
  " packageName, npmMetadata, githubMetadata, webPresence, sourceFiles",
21
22
  "",
22
- "--reason consults an LLM as an authoritative verdict on top of the static",
23
- "heuristics. Provider auto-detected from --reason-model, or pass",
24
- "--reason-provider <anthropic|openai|gemini>. Defaults: anthropic +",
25
- "claude-opus-4-7. Each provider needs its own env key (ANTHROPIC_API_KEY,",
26
- "OPENAI_API_KEY, GEMINI_API_KEY) and SDK installed.",
23
+ "LLM reasoning runs automatically when any of ANTHROPIC_API_KEY,",
24
+ "OPENAI_API_KEY, or GEMINI_API_KEY is set AND the matching SDK is",
25
+ "installed (@anthropic-ai/sdk, openai, or @google/generative-ai).",
26
+ "Use --no-reason to force static-only, --reason-provider <name> to pin a",
27
+ "provider, --reason-model <id> to pin a model.",
27
28
  ""
28
29
  ].join("\n")
29
30
  );
@@ -59,6 +60,9 @@ function parseArgs(argv) {
59
60
  options.vulnerabilityCheck = false;
60
61
  } else if (arg === "--reason") {
61
62
  options.reason = true;
63
+ } else if (arg === "--no-reason") {
64
+ options.reason = false;
65
+ options.reasonExplicitOff = true;
62
66
  } else if (arg === "--reason-model") {
63
67
  options.reasonModel = argv[++i];
64
68
  } else if (arg === "--reason-provider") {
@@ -82,11 +86,21 @@ function readInput(file) {
82
86
  return fs.readFileSync(0, "utf8");
83
87
  }
84
88
 
85
- async function maybeReason(evidence, options) {
86
- if (!options.reason) return null;
89
+ function resolveReasonMode(options) {
90
+ if (options.reasonExplicitOff) return { enabled: false, autoDetected: false };
91
+ if (options.reason) return { enabled: true, autoDetected: false };
92
+ const detected = detectAvailableProvider();
93
+ if (detected) {
94
+ return { enabled: true, autoDetected: true, detectedProvider: detected.name };
95
+ }
96
+ return { enabled: false, autoDetected: false };
97
+ }
98
+
99
+ async function maybeReason(evidence, options, mode) {
100
+ if (!mode || !mode.enabled) return null;
87
101
  try {
88
102
  return await reasonAbout(evidence, {
89
- provider: options.reasonProvider,
103
+ provider: options.reasonProvider || mode.detectedProvider,
90
104
  model: options.reasonModel,
91
105
  maxFiles: options.reasonMaxFiles
92
106
  });
@@ -114,7 +128,8 @@ async function main() {
114
128
  throw new Error("guard requires an extension reference");
115
129
  }
116
130
  const result = await guardExtension(options.reference, options);
117
- if (options.reason) {
131
+ const reasonMode = resolveReasonMode(options);
132
+ if (reasonMode.enabled) {
118
133
  const evidenceForReason = {
119
134
  packageName: result.resolved && result.resolved.packageName,
120
135
  npmMetadata: result.resolved && result.resolved.npmMetadata,
@@ -122,7 +137,7 @@ async function main() {
122
137
  webPresence: null,
123
138
  sourceFiles: result.sourceFiles || {}
124
139
  };
125
- const reasoning = await maybeReason(evidenceForReason, options);
140
+ const reasoning = await maybeReason(evidenceForReason, options, reasonMode);
126
141
  result.reasoning = reasoning;
127
142
  if (reasoning && !reasoning.error && reasoning.verdict) {
128
143
  result.decision = reasoning.verdict === "block"
@@ -131,6 +146,8 @@ async function main() {
131
146
  ? "allow"
132
147
  : "review";
133
148
  }
149
+ } else {
150
+ result.reasoningHint = reasoningSetupHint();
134
151
  }
135
152
  if (options.format === "json") {
136
153
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -148,14 +165,22 @@ async function main() {
148
165
 
149
166
  const evidence = JSON.parse(raw);
150
167
  const report = auditEvidence(evidence);
151
- const reasoning = await maybeReason(evidence, options);
168
+ const reasonMode = resolveReasonMode(options);
169
+ const reasoning = await maybeReason(evidence, options, reasonMode);
170
+ const hint = reasonMode.enabled ? null : reasoningSetupHint();
152
171
 
153
- const payload = reasoning ? { report, reasoning } : report;
172
+ const payload = reasoning
173
+ ? { report, reasoning }
174
+ : hint
175
+ ? { report, reasoningHint: hint }
176
+ : report;
154
177
 
155
178
  if (options.format === "json") {
156
179
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
157
180
  } else if (reasoning) {
158
181
  process.stdout.write(`${renderMarkdown(report)}\n\n---\n\n${renderReasoningMarkdown(reasoning)}\n`);
182
+ } else if (hint) {
183
+ process.stdout.write(`${renderMarkdown(report)}\n\nšŸ’” ${hint}\n`);
159
184
  } else {
160
185
  process.stdout.write(`${renderMarkdown(report)}\n`);
161
186
  }
@@ -182,6 +207,8 @@ function renderGuardMarkdown(result) {
182
207
 
183
208
  if (result.reasoning) {
184
209
  lines.push("", "---", "", renderReasoningMarkdown(result.reasoning));
210
+ } else if (result.reasoningHint) {
211
+ lines.push("", `šŸ’” ${result.reasoningHint}`);
185
212
  }
186
213
 
187
214
  return lines.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkgxray",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Local CLI and MCP server that audits AI-agent extensions and npm packages for supply-chain risk. Zero-dep static heuristics + sandboxed quarantine + optional multi-provider (Claude / GPT / Gemini) reasoning layer.",
5
5
  "license": "MIT",
6
6
  "author": "Jack Adams-Lovell",
@@ -37,4 +37,61 @@ function resolveProvider({ provider, model } = {}) {
37
37
  return anthropic;
38
38
  }
39
39
 
40
- module.exports = { PROVIDERS, listProviders, getProvider, detectProvider, resolveProvider };
40
+ function tryLoadSdk(provider) {
41
+ try {
42
+ if (typeof provider._loadSdk === "function") {
43
+ provider._loadSdk();
44
+ return true;
45
+ }
46
+ // Each provider lazy-loads inside call(); fall back to a probe require here.
47
+ if (provider.name === "anthropic") require("@anthropic-ai/sdk");
48
+ else if (provider.name === "openai") require("openai");
49
+ else if (provider.name === "gemini") require("@google/generative-ai");
50
+ return true;
51
+ } catch (error) {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function detectAvailableProvider() {
57
+ // Priority order: anthropic, openai, gemini. First one with both env key set
58
+ // AND SDK loadable wins.
59
+ for (const name of ["anthropic", "openai", "gemini"]) {
60
+ const provider = PROVIDERS[name];
61
+ const keyPresent = Boolean(process.env[provider.envKey]);
62
+ if (!keyPresent) continue;
63
+ if (!tryLoadSdk(provider)) continue;
64
+ return provider;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ function reasoningSetupHint() {
70
+ const missing = [];
71
+ for (const name of ["anthropic", "openai", "gemini"]) {
72
+ const provider = PROVIDERS[name];
73
+ if (process.env[provider.envKey]) {
74
+ if (!tryLoadSdk(provider)) {
75
+ const pkg = provider.name === "anthropic"
76
+ ? "@anthropic-ai/sdk"
77
+ : provider.name === "openai"
78
+ ? "openai"
79
+ : "@google/generative-ai";
80
+ return `${provider.envKey} is set but ${pkg} is not installed. Run: npm install -g ${pkg}`;
81
+ }
82
+ } else {
83
+ missing.push(provider.envKey);
84
+ }
85
+ }
86
+ return `For LLM-grade verdicts, set one of ${missing.join(" / ")} and install the matching SDK (@anthropic-ai/sdk, openai, or @google/generative-ai).`;
87
+ }
88
+
89
+ module.exports = {
90
+ PROVIDERS,
91
+ listProviders,
92
+ getProvider,
93
+ detectProvider,
94
+ resolveProvider,
95
+ detectAvailableProvider,
96
+ reasoningSetupHint
97
+ };