@wrongstack/plugins 0.281.0 → 0.281.1

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.
@@ -18,6 +18,31 @@ var DEFAULTS = {
18
18
  timeoutMs: 5e3,
19
19
  maxConsecutiveFailures: 5
20
20
  };
21
+ function isPrivateIPv4(hostname) {
22
+ const parts = hostname.split(".").map((p) => Number(p));
23
+ if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
24
+ return false;
25
+ }
26
+ const [a, b] = parts;
27
+ return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
28
+ }
29
+ function isBlockedHostname(hostname) {
30
+ const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
31
+ return h === "localhost" || h.endsWith(".localhost") || h === "::1" || h === "0:0:0:0:0:0:0:1" || h.startsWith("fc") || h.startsWith("fd") || h.startsWith("fe80:") || isPrivateIPv4(h);
32
+ }
33
+ function normalizeWebhookUrl(raw) {
34
+ if (typeof raw !== "string" || raw.trim().length === 0) return "";
35
+ try {
36
+ const url = new URL(raw.trim());
37
+ if (url.protocol !== "https:" && url.protocol !== "http:") return "";
38
+ if (url.username || url.password) return "";
39
+ if (!url.hostname || isBlockedHostname(url.hostname)) return "";
40
+ url.hash = "";
41
+ return url.toString();
42
+ } catch {
43
+ return "";
44
+ }
45
+ }
21
46
  function readConfig(raw) {
22
47
  if (!raw || typeof raw !== "object") return { ...DEFAULTS, events: [...DEFAULTS.events] };
23
48
  const r = raw;
@@ -29,7 +54,7 @@ function readConfig(raw) {
29
54
  }
30
55
  return {
31
56
  enabled: r["enabled"] !== false,
32
- webhookUrl: typeof r["webhookUrl"] === "string" ? r["webhookUrl"] : "",
57
+ webhookUrl: normalizeWebhookUrl(r["webhookUrl"]),
33
58
  events: Array.isArray(r["events"]) ? r["events"].filter((e) => KNOWN_EVENTS.includes(e)) : [...DEFAULTS.events],
34
59
  headers,
35
60
  timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] >= 500 && r["timeoutMs"] <= 6e4 ? r["timeoutMs"] : DEFAULTS.timeoutMs,
@@ -2,10 +2,18 @@ import { expectDefined } from '@wrongstack/core';
2
2
  import { toErrorMessage } from '@wrongstack/core/utils';
3
3
  import { execFileSync } from 'child_process';
4
4
  import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
5
- import { join } from 'path';
5
+ import { join, resolve, isAbsolute, relative } from 'path';
6
6
 
7
7
  // src/semver-bump/index.ts
8
8
  var API_VERSION = "^0.1.10";
9
+ function resolveProjectRoot(rawCwd, root = process.cwd()) {
10
+ if (typeof rawCwd !== "string" || rawCwd.length === 0) return root;
11
+ const base = resolve(root);
12
+ const resolved = isAbsolute(rawCwd) ? resolve(rawCwd) : resolve(base, rawCwd);
13
+ const rel = relative(base, resolved);
14
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return resolved;
15
+ return null;
16
+ }
9
17
  var state = {
10
18
  /** Total invocations across all three tools this session. */
11
19
  invocationCount: 0,
@@ -56,7 +64,11 @@ function collectManifests(root) {
56
64
  function parseVersion(v) {
57
65
  const m = v.match(/^v?(\d+)\.(\d+)\.(\d+)/);
58
66
  if (!m) return [0, 0, 0];
59
- return [Number.parseInt(expectDefined(m[1]), 10), Number.parseInt(expectDefined(m[2]), 10), Number.parseInt(expectDefined(m[3]), 10)];
67
+ return [
68
+ Number.parseInt(expectDefined(m[1]), 10),
69
+ Number.parseInt(expectDefined(m[2]), 10),
70
+ Number.parseInt(expectDefined(m[3]), 10)
71
+ ];
60
72
  }
61
73
  function bumpVersion(version, part) {
62
74
  let [major, minor, patch] = parseVersion(version);
@@ -192,6 +204,11 @@ var plugin = {
192
204
  defaultPart = readDefaultPart(next);
193
205
  });
194
206
  async function performBump(part, dryRun, cwd) {
207
+ const safeCwd = resolveProjectRoot(cwd);
208
+ if (!safeCwd) {
209
+ return { ok: false, error: "cwd must stay within the current project directory" };
210
+ }
211
+ cwd = safeCwd;
195
212
  const pkg = getPackageJson(cwd);
196
213
  if (!pkg) {
197
214
  return { ok: false, error: "No package.json found" };
@@ -299,7 +316,12 @@ var plugin = {
299
316
  properties: {
300
317
  cwd: { type: "string", description: "Working directory (defaults to project root)" },
301
318
  dry_run: { type: "boolean", default: false },
302
- part: { type: "string", enum: ["major", "minor", "patch", "auto"], default: defaultPart, description: "Version part to bump. Omitted \u2192 the configured default (/settings semver-part, factory default: patch). Use auto to infer from commits." }
319
+ part: {
320
+ type: "string",
321
+ enum: ["major", "minor", "patch", "auto"],
322
+ default: defaultPart,
323
+ description: "Version part to bump. Omitted \u2192 the configured default (/settings semver-part, factory default: patch). Use auto to infer from commits."
324
+ }
303
325
  }
304
326
  },
305
327
  permission: "confirm",
@@ -363,7 +385,11 @@ var plugin = {
363
385
  if (mode !== "patch" && mode !== "minor" && mode !== "major" && mode !== "auto") {
364
386
  return { message: `Unknown mode "${mode}". Use status, patch, minor, major or auto.` };
365
387
  }
366
- const result = await performBump(mode, dry, cwd);
388
+ const safeCwd = resolveProjectRoot(cwd);
389
+ if (!safeCwd) {
390
+ return { message: "cwd must stay within the current project directory" };
391
+ }
392
+ const result = await performBump(mode, dry, safeCwd);
367
393
  return { message: String(result["message"] ?? result["error"] ?? JSON.stringify(result)) };
368
394
  }
369
395
  });
@@ -381,16 +407,20 @@ var plugin = {
381
407
  async execute(input) {
382
408
  state.invocationCount += 1;
383
409
  state.perTool["semver_current"] = (state.perTool["semver_current"] ?? 0) + 1;
384
- const cwd = input["cwd"];
385
- const pkg = getPackageJson(cwd);
410
+ const cwdInput = input["cwd"];
411
+ const safeCwd = resolveProjectRoot(cwdInput);
412
+ if (!safeCwd) {
413
+ return { ok: false, error: "cwd must stay within the current project directory" };
414
+ }
415
+ const pkg = getPackageJson(safeCwd);
386
416
  const currentVersion = pkg?.version ?? "unknown";
387
417
  let latestTag = null;
388
418
  let commitsSinceTag = 0;
389
419
  try {
390
- const tagsOutput = runGit(["describe", "--tags", "--abbrev=0"], cwd);
420
+ const tagsOutput = runGit(["describe", "--tags", "--abbrev=0"], safeCwd);
391
421
  latestTag = tagsOutput || null;
392
422
  if (latestTag) {
393
- const countOutput = runGit(["rev-list", "--count", `${latestTag}..HEAD`], cwd);
423
+ const countOutput = runGit(["rev-list", "--count", `${latestTag}..HEAD`], safeCwd);
394
424
  commitsSinceTag = Number.parseInt(countOutput, 10) || 0;
395
425
  }
396
426
  } catch {
@@ -425,11 +455,15 @@ var plugin = {
425
455
  const from = input["from"];
426
456
  const to = input["to"] ?? "HEAD";
427
457
  const cwd = input["cwd"];
458
+ const safeCwd = resolveProjectRoot(cwd);
459
+ if (!safeCwd) {
460
+ return { ok: false, error: "cwd must stay within the current project directory" };
461
+ }
428
462
  const format = input["format"] ?? "markdown";
429
463
  const range = from ? `${from}..${to}` : to;
430
464
  let commits;
431
465
  try {
432
- const output = runGit(["log", range === to ? "-30" : range, "--format=%H %s"], cwd);
466
+ const output = runGit(["log", range === to ? "-30" : range, "--format=%H %s"], safeCwd);
433
467
  commits = output.split("\n").filter(Boolean).map((line) => {
434
468
  const spaceIdx = line.indexOf(" ");
435
469
  const hash = line.slice(0, spaceIdx);
@@ -1,7 +1,7 @@
1
1
  import { existsSync, statSync } from 'fs';
2
2
  import * as fsp from 'fs/promises';
3
- import 'child_process';
4
3
  import 'path';
4
+ import 'child_process';
5
5
  import '@wrongstack/core';
6
6
  import 'os';
7
7
  import 'crypto';
@@ -292,9 +292,26 @@ function isWrappedAsLinkOrCode(line, name) {
292
292
  function escapeRegExp(s) {
293
293
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
294
294
  }
295
+ function mapMarkdownFences(lines) {
296
+ const fenced = new Array(lines.length).fill(false);
297
+ let inFence = false;
298
+ for (let i = 0; i < lines.length; i++) {
299
+ const line = lines[i];
300
+ if (/^\s*```/.test(line)) {
301
+ fenced[i] = true;
302
+ inFence = !inFence;
303
+ continue;
304
+ }
305
+ fenced[i] = inFence;
306
+ }
307
+ return fenced;
308
+ }
295
309
  function findUnlinkedReferences(lines, names) {
296
310
  const found = /* @__PURE__ */ new Map();
297
- for (const line of lines) {
311
+ const fencedLines = mapMarkdownFences(lines);
312
+ for (let i = 0; i < lines.length; i++) {
313
+ if (fencedLines[i]) continue;
314
+ const line = lines[i];
298
315
  if (line.length === 0) continue;
299
316
  for (const name of names) {
300
317
  const re = new RegExp(`(^|[^\\w-])${escapeRegExp(name)}(?![\\w-])`, "i");
@@ -307,8 +324,10 @@ function findUnlinkedReferences(lines, names) {
307
324
  }
308
325
  function wrapUnlinkedReferences(content) {
309
326
  const lines = content.split("\n");
327
+ const fencedLines = mapMarkdownFences(lines);
310
328
  let changed = false;
311
329
  for (let i = 0; i < lines.length; i++) {
330
+ if (fencedLines[i]) continue;
312
331
  const line = lines[i];
313
332
  if (line.length === 0) continue;
314
333
  const newLine = wrapLineReferences(line);
@@ -424,7 +443,9 @@ var plugin36 = {
424
443
  state32.unlinkedCount += 1;
425
444
  const limited = unlinked.slice(0, cfg.maxReferences);
426
445
  const overflow = unlinked.length - limited.length;
427
- const lines = limited.map((name) => `- \`${name}\` \u2192 \`[${name}](${PLUGIN_CATALOG.get(name) ?? `./src/${name}`})\``).join("\n");
446
+ const lines = limited.map(
447
+ (name) => `- \`${name}\` \u2192 \`[${name}](${PLUGIN_CATALOG.get(name) ?? `./src/${name}`})\``
448
+ ).join("\n");
428
449
  const overflowNote = overflow > 0 ? `
429
450
  - \u2026and ${overflow} more` : "";
430
451
  return {
@@ -13,27 +13,21 @@ function expandTemplate(template, variables) {
13
13
  return result;
14
14
  }
15
15
  function expandConditionals(template, variables) {
16
- return template.replace(
17
- /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
18
- (_, key, content) => {
19
- const val = variables[key];
20
- return val !== void 0 && val !== "" && val !== "false" && val !== "0" ? content : "";
21
- }
22
- );
16
+ return template.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, content) => {
17
+ const val = variables[key];
18
+ return val !== void 0 && val !== "" && val !== "false" && val !== "0" ? content : "";
19
+ });
23
20
  }
24
21
  function expandLoops(template, variables) {
25
- return template.replace(
26
- /\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
27
- (_, key, content) => {
28
- const val = variables[key];
29
- if (!val) return "";
30
- if (typeof val === "string" && val.includes(",")) {
31
- const items = val.split(",").map((s) => s.trim());
32
- return items.map((item) => expandTemplate(content, { ...variables, [key]: item })).join("\n");
33
- }
34
- return expandTemplate(content, variables);
22
+ return template.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, key, content) => {
23
+ const val = variables[key];
24
+ if (!val) return "";
25
+ if (typeof val === "string" && val.includes(",")) {
26
+ const items = val.split(",").map((s) => s.trim());
27
+ return items.map((item) => expandTemplate(content, { ...variables, [key]: item })).join("\n");
35
28
  }
36
- );
29
+ return expandTemplate(content, variables);
30
+ });
37
31
  }
38
32
  function renderTemplate(template, variables, escapeHtml = true) {
39
33
  let result = template;
@@ -52,6 +46,12 @@ function renderTemplateRaw(template, variables) {
52
46
  result = expandTemplate(result, variables);
53
47
  return result;
54
48
  }
49
+ function validateRelativeTemplatePath(field, value) {
50
+ if (isAbsolute(value) || value.split(/[\\/]+/).includes("..")) {
51
+ return `${field} must be a relative path without ".." components`;
52
+ }
53
+ return null;
54
+ }
55
55
  var plugin = {
56
56
  name: "template-engine",
57
57
  version: "0.1.0",
@@ -80,13 +80,19 @@ var plugin = {
80
80
  inputSchema: {
81
81
  type: "object",
82
82
  properties: {
83
- template: { type: "string", description: "Template string with {{variable}} placeholders" },
83
+ template: {
84
+ type: "string",
85
+ description: "Template string with {{variable}} placeholders"
86
+ },
84
87
  variables: {
85
88
  type: "object",
86
89
  description: "Variables to substitute into the template",
87
90
  additionalProperties: { type: "string" }
88
91
  },
89
- output_path: { type: "string", description: "Optional path to write the expanded result" },
92
+ output_path: {
93
+ type: "string",
94
+ description: "Optional path to write the expanded result"
95
+ },
90
96
  raw: { type: "boolean", default: false, description: "Disable HTML auto-escaping" }
91
97
  },
92
98
  required: ["template", "variables"]
@@ -112,9 +118,8 @@ var plugin = {
112
118
  return { ok: false, error: String(err) };
113
119
  }
114
120
  if (output_path) {
115
- if (isAbsolute(output_path) || output_path.includes("..")) {
116
- return { ok: false, error: 'output_path must be a relative path without ".." components' };
117
- }
121
+ const pathError = validateRelativeTemplatePath("output_path", output_path);
122
+ if (pathError) return { ok: false, error: pathError };
118
123
  const { writeFileSync } = await import('fs');
119
124
  writeFileSync(output_path, result, "utf-8");
120
125
  return {
@@ -144,7 +149,10 @@ var plugin = {
144
149
  description: "Variables to substitute",
145
150
  additionalProperties: { type: "string" }
146
151
  },
147
- output_path: { type: "string", description: "Optional path to write the rendered result" },
152
+ output_path: {
153
+ type: "string",
154
+ description: "Optional path to write the rendered result"
155
+ },
148
156
  raw: { type: "boolean", default: false }
149
157
  },
150
158
  required: ["template_path", "variables"]
@@ -159,6 +167,8 @@ var plugin = {
159
167
  if (!template_path || typeof template_path !== "string") {
160
168
  return { ok: false, error: "template_path is required and must be a string" };
161
169
  }
170
+ const templatePathError = validateRelativeTemplatePath("template_path", template_path);
171
+ if (templatePathError) return { ok: false, error: templatePathError };
162
172
  if (!variables || typeof variables !== "object") {
163
173
  return { ok: false, error: "variables is required and must be an object" };
164
174
  }
@@ -176,9 +186,8 @@ var plugin = {
176
186
  return { ok: false, error: `Template rendering failed: ${err}` };
177
187
  }
178
188
  if (output_path) {
179
- if (isAbsolute(output_path) || output_path.includes("..")) {
180
- return { ok: false, error: 'output_path must be a relative path without ".." components' };
181
- }
189
+ const pathError = validateRelativeTemplatePath("output_path", output_path);
190
+ if (pathError) return { ok: false, error: pathError };
182
191
  const { writeFileSync } = await import('fs');
183
192
  writeFileSync(output_path, result, "utf-8");
184
193
  return {
@@ -203,8 +212,14 @@ var plugin = {
203
212
  type: "object",
204
213
  properties: {
205
214
  name: { type: "string", description: "Unique name for this template" },
206
- content: { type: "string", description: "Template content with {{variable}} placeholders" },
207
- description: { type: "string", description: "Optional description of what this template is for" }
215
+ content: {
216
+ type: "string",
217
+ description: "Template content with {{variable}} placeholders"
218
+ },
219
+ description: {
220
+ type: "string",
221
+ description: "Optional description of what this template is for"
222
+ }
208
223
  },
209
224
  required: ["name", "content"]
210
225
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/plugins",
3
- "version": "0.281.0",
3
+ "version": "0.281.1",
4
4
  "description": "Official WrongStack plugin collection — auto-doc, git-autocommit, shell-check, cost-tracker, file-watcher, cron, template-engine, semver-bump, secret-scanner, todo-tracker, token-budget, lint-gate, branch-guard, diff-summary, commit-validator, format-on-save, test-runner-gate, import-organizer, todo-listener, session-recap, spec-linker, loop-breaker, path-guard, context-pins, checkpoint, error-lens, dep-guard, config-validator, notify-hub, changelog-writer, injection-shield, llm-cache, model-router, prompt-firewall, auto-escalate, token-throttle",
5
5
  "license": "MIT",
6
6
  "author": "ECOSTACK TECHNOLOGY OÜ",
@@ -167,8 +167,8 @@
167
167
  "vitest": "^4.1.9"
168
168
  },
169
169
  "dependencies": {
170
- "@wrongstack/core": "0.281.0",
171
- "@wrongstack/tools": "0.281.0"
170
+ "@wrongstack/core": "0.281.1",
171
+ "@wrongstack/tools": "0.281.1"
172
172
  },
173
173
  "scripts": {
174
174
  "build": "tsup",