@wrongstack/plugins 0.299.0 → 0.301.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/README.md CHANGED
@@ -725,7 +725,9 @@ warning depending on `mode`.
725
725
 
726
726
  Protects sensitive paths from accidental writes/edits and destructive shell
727
727
  commands. Defaults cover lockfiles, `.env`, `.git`, and migration-like paths;
728
- use `mode: "warn"` for advisory-only enforcement.
728
+ use `mode: "warn"` for advisory-only enforcement. Writer globs are blocked when
729
+ their unresolved scope overlaps a protected path; narrow the target or add an
730
+ `allow` glob to exempt an intentional scope.
729
731
 
730
732
  ```jsonc
731
733
  {
@@ -265,7 +265,7 @@ export declare const OFFICIAL_PLUGIN_AUDIT_ENTRIES: readonly [{
265
265
  }, {
266
266
  readonly name: 'migration-planner';
267
267
  readonly risk: 'low';
268
- readonly summary: 'Builds evidence-backed migration checklists with optional host-routed LLM risk analysis';
268
+ readonly summary: 'Builds evidence-backed migration checklists with optional Council-reviewed risk analysis';
269
269
  readonly defaultState: 'inactive';
270
270
  readonly canDisable: true;
271
271
  }, {
package/dist/audit.js CHANGED
@@ -311,7 +311,7 @@ var OFFICIAL_PLUGIN_AUDIT_ENTRIES = [
311
311
  {
312
312
  name: "migration-planner",
313
313
  risk: "low",
314
- summary: "Builds evidence-backed migration checklists with optional host-routed LLM risk analysis",
314
+ summary: "Builds evidence-backed migration checklists with optional Council-reviewed risk analysis",
315
315
  defaultState: "inactive",
316
316
  canDisable: true
317
317
  },
package/dist/auto-doc.js CHANGED
@@ -106,6 +106,7 @@ async function generateDocCommentLlm(entity, snippet, includeTypes, api) {
106
106
  `Write documentation for this ${entity.kind} named "${entity.name}". Respond with ONLY a JSON object of the form {"summary": string, "params": {"<name>": string}, "returns": string}. summary is one concise sentence. params has one entry per parameter` + (paramList.length > 0 ? ` (${paramList.join(", ")})` : " (may be empty)") + ". returns describes the return value (empty string if none). No prose outside the JSON.\n\n```\n" + snippet + "\n```",
107
107
  {
108
108
  system: "You are a precise API documentation writer. Output only JSON.",
109
+ role: "document",
109
110
  maxTokens: 400,
110
111
  responseFormat: "json"
111
112
  }
@@ -320,7 +320,11 @@ var plugin = {
320
320
  try {
321
321
  const result = await api.llm.complete(
322
322
  'Rewrite this Keep-a-Changelog block into concise, user-facing release-notes wording. Keep the EXACT markdown structure: the same "### Section" headings, one "- " bullet per entry, no entries added or removed. Output ONLY the markdown block.\n\n' + block,
323
- { system: "You are a precise technical release-notes editor.", maxTokens: 1500 }
323
+ {
324
+ system: "You are a precise technical release-notes editor.",
325
+ role: "document",
326
+ maxTokens: 1500
327
+ }
324
328
  );
325
329
  const text = result.text.trim();
326
330
  if (!text.startsWith("###")) return block;
@@ -300,6 +300,7 @@ Errors: ${parsed.errors.join("; ")}
300
300
  Reply with ONE corrected conventional-commit subject line (and optional body) and nothing else.`,
301
301
  {
302
302
  system: "You rewrite commit subjects to follow the conventional-commits format. Reply tersely, no preamble, no quotes.",
303
+ role: "reviewer",
303
304
  maxTokens: 120
304
305
  }
305
306
  );
@@ -1,5 +1,30 @@
1
1
  // src/config-validator/index.ts
2
2
  import { readFileSync, statSync } from "node:fs";
3
+
4
+ // src/runtime/index.ts
5
+ import { basename, extname, isAbsolute, relative, resolve } from "node:path";
6
+
7
+ // src/runtime/local-bin.ts
8
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
9
+
10
+ // src/runtime/index.ts
11
+ var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
12
+ function hasLeadingDash(arg) {
13
+ return arg.length > 0 && arg.startsWith("-");
14
+ }
15
+ function withinProjectPath(projectRoot, candidate) {
16
+ if (candidate.length === 0 || candidate.length > 4096) return false;
17
+ if (hasLeadingDash(candidate)) return false;
18
+ const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(projectRoot, candidate);
19
+ const rel = relative(projectRoot, resolved);
20
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
21
+ }
22
+ function withinProject(p) {
23
+ const cwd = process.cwd();
24
+ return withinProjectPath(cwd, p) || relative(cwd, p) === ".";
25
+ }
26
+
27
+ // src/config-validator/index.ts
3
28
  var state = {
4
29
  invocations: 0,
5
30
  filesChecked: 0,
@@ -79,6 +104,9 @@ function positionToLineCol(text, pos) {
79
104
  const col = pos - upTo.lastIndexOf("\n");
80
105
  return { line, col };
81
106
  }
107
+ function redactParseSnippet(message) {
108
+ return message.replace(/"[\s\S]{0,200}?"(?= is not valid JSON)/g, '"\u2026"');
109
+ }
82
110
  function validateJson(text, isJsonc, fileName) {
83
111
  const source = isJsonc ? stripJsonc(text) : text;
84
112
  try {
@@ -112,10 +140,10 @@ function validateJson(text, isJsonc, fileName) {
112
140
  const idx = source.indexOf(snippetMatch[1]);
113
141
  if (idx >= 0) {
114
142
  const { line } = positionToLineCol(source, idx);
115
- return [`JSON parse error near line ${line}: ${message.split("\n")[0]}`];
143
+ return [`JSON parse error near line ${line}`];
116
144
  }
117
145
  }
118
- return [`JSON parse error: ${message.split("\n")[0]}`];
146
+ return [`JSON parse error: ${redactParseSnippet(message.split("\n")[0] ?? "")}`];
119
147
  }
120
148
  }
121
149
  function validateYaml(text) {
@@ -252,10 +280,12 @@ var plugin = {
252
280
  const cfg = readConfig(api.config.extensions?.["config-validator"]);
253
281
  const hook = (input) => {
254
282
  if (!cfg.enabled) return;
283
+ if (input.toolResult?.isError) return;
255
284
  state.invocations += 1;
256
285
  const ti = input.toolInput ?? {};
257
286
  const raw = ti["path"] ?? ti["file_path"] ?? ti["filePath"];
258
287
  if (typeof raw !== "string" || raw.length === 0) return;
288
+ if (!withinProject(raw)) return;
259
289
  const lower = raw.toLowerCase();
260
290
  if (!cfg.extensions.some((ext) => lower.endsWith(ext))) return;
261
291
  let text;
package/dist/dep-guard.js CHANGED
@@ -175,7 +175,7 @@ var plugin = {
175
175
  confirmTyposquatsWithLlm: {
176
176
  type: "boolean",
177
177
  default: false,
178
- description: "Ask the host LLM to confirm whether a flagged typosquat is real-but-obscure or genuinely a typo. Appended to the warn context, never escalates to a block. Off by default."
178
+ description: "Ask the risk-review Council to assess a flagged typosquat, with One Shot fallback. Appended to the warn context, never escalates to a block. Off by default."
179
179
  }
180
180
  }
181
181
  },
@@ -234,14 +234,25 @@ var plugin = {
234
234
  const baseNote = `"${pkg.name}" is one edit away from the well-known package "${lookalike}" \u2014 possible typosquat. Verify the name before installing.`;
235
235
  if (cfg.confirmTyposquatsWithLlm && api.llm) {
236
236
  try {
237
- const verdict = await api.llm.complete(
238
- `A user is installing the npm package "${pkg.name}" which is 1 edit away from the well-known "${lookalike}". Is "${pkg.name}" likely a typo of "${lookalike}" or a real-but-obscure package? Reply with ONE sentence starting with "TYPO:" or "REAL:".`,
237
+ const question = `Classify whether npm package "${pkg.name}" is likely a typo or typosquat of "${lookalike}".`;
238
+ const council = api.llm.council ? await api.llm.council(question, {
239
+ context: "The only supplied evidence is that the names have edit distance 1. Do not invent registry, download, ownership, or provenance facts.",
240
+ profile: "risk-review",
241
+ options: [
242
+ { id: "typo", label: "Likely typo or typosquat" },
243
+ { id: "real", label: "Likely distinct real package" },
244
+ { id: "uncertain", label: "Insufficient evidence" }
245
+ ]
246
+ }) : null;
247
+ const councilVerdict = council?.status === "decided" && council.optionId ? `${council.optionId.toUpperCase()}: ${council.reason ?? council.answer ?? "no rationale"}` : null;
248
+ const t = councilVerdict ? councilVerdict : (await api.llm.complete(
249
+ `${question} Reply with ONE sentence starting with "TYPO:", "REAL:", or "UNCERTAIN:".`,
239
250
  {
240
- system: "You are a supply-chain security assistant. Reply tersely with a single TYPO: or REAL: verdict.",
241
- maxTokens: 80
251
+ system: "You are a supply-chain security assistant. Use only supplied evidence and preserve uncertainty.",
252
+ role: "security-reviewer",
253
+ maxTokens: 100
242
254
  }
243
- );
244
- const t = verdict.text.trim();
255
+ )).text.trim();
245
256
  if (t) {
246
257
  state.llmConfirmCount += 1;
247
258
  api.metrics.counter("llm_confirm");
@@ -190,7 +190,11 @@ var plugin = {
190
190
  ${errorLine ?? "(no error line)"}
191
191
  ` + (frames.length > 0 ? `Top stack frames: ${frames.join(", ")}
192
192
  ` : "") + "Reply with ONE short sentence suggesting the most likely fix. No preamble.",
193
- { system: "You are a terse debugging assistant.", maxTokens: 100 }
193
+ {
194
+ system: "You are a terse debugging assistant.",
195
+ role: "reviewer",
196
+ maxTokens: 100
197
+ }
194
198
  );
195
199
  const text = hint.text.trim();
196
200
  if (text) {
@@ -22,11 +22,7 @@ async function runGit(args, cwd, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
22
22
  (err, stdout) => {
23
23
  if (err) {
24
24
  const e = err;
25
- rejectPromise(
26
- new Error(
27
- `git command failed: ${e.message ?? e.stderr ?? String(err)}`
28
- )
29
- );
25
+ rejectPromise(new Error(`git command failed: ${e.message ?? e.stderr ?? String(err)}`));
30
26
  return;
31
27
  }
32
28
  resolvePromise(stdout.trim());
@@ -198,6 +194,7 @@ Diff:
198
194
  ${diff}`,
199
195
  {
200
196
  system: "You are a precise release engineer writing Conventional Commits. Output only JSON.",
197
+ role: "document",
201
198
  maxTokens: 400,
202
199
  responseFormat: "json"
203
200
  }