@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.
@@ -6,13 +6,15 @@ var API_VERSION = "^0.1.10";
6
6
  var commitCount = { value: 0 };
7
7
  var lastCommit = { hash: null, at: null };
8
8
  var llmGenerated = { value: 0 };
9
- function runGit(args, cwd) {
9
+ var DEFAULT_GIT_TIMEOUT_MS = 3e4;
10
+ var GIT_COMMIT_TIMEOUT_MS = 5 * 6e4;
11
+ function runGit(args, cwd, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
10
12
  try {
11
13
  return execFileSync("git", args, {
12
14
  encoding: "utf-8",
13
15
  cwd,
14
16
  stdio: ["pipe", "pipe", "pipe"],
15
- timeout: 3e4,
17
+ timeout: timeoutMs,
16
18
  maxBuffer: 10 * 1024 * 1024,
17
19
  windowsHide: true
18
20
  }).trim();
@@ -43,7 +45,7 @@ function stageFiles(files, cwd) {
43
45
  runGit(["add", ...existing], cwd);
44
46
  }
45
47
  function commitWithMessage(message, cwd) {
46
- return runGit(["commit", "-m", message], cwd);
48
+ return runGit(["commit", "-m", message], cwd, GIT_COMMIT_TIMEOUT_MS);
47
49
  }
48
50
  function getWorktrees(cwd) {
49
51
  try {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
+ import * as path from 'path';
2
+ import { dirname, resolve, isAbsolute, relative, join, basename } from 'path';
1
3
  import { execSync, execFileSync, spawn } from 'child_process';
2
4
  import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, watch, readdirSync, mkdtempSync, rmSync } from 'fs';
3
- import * as path from 'path';
4
- import { dirname, isAbsolute, join, basename } from 'path';
5
5
  import { expectDefined } from '@wrongstack/core';
6
6
  import { tmpdir } from 'os';
7
7
  import { randomUUID, createHash } from 'crypto';
@@ -10,6 +10,14 @@ import * as fsp from 'fs/promises';
10
10
 
11
11
  // src/auto-doc/index.ts
12
12
  var AUTO_DOC_API_VERSION = "^0.1.10";
13
+ function resolveProjectPath(rawPath, cwd = process.cwd()) {
14
+ if (typeof rawPath !== "string" || rawPath.length === 0) return null;
15
+ const root = resolve(cwd);
16
+ const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
17
+ const rel = relative(root, resolved);
18
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return resolved;
19
+ return null;
20
+ }
13
21
  var state = {
14
22
  invocationCount: 0,
15
23
  /** Doc comments whose prose came from the LLM this session. */
@@ -180,14 +188,19 @@ async function runAutoDoc(input, api) {
180
188
  const maxLlmEntities = typeof extConfig["maxLlmEntities"] === "number" && extConfig["maxLlmEntities"] >= 0 ? extConfig["maxLlmEntities"] : 25;
181
189
  const results = [];
182
190
  let llmBudget = maxLlmEntities;
183
- for (const file of input.files) {
191
+ for (const rawFile of input.files) {
192
+ const safeFile = resolveProjectPath(rawFile);
193
+ if (!safeFile) {
194
+ api.log.warn(`auto-doc: skipped file outside project directory: ${rawFile}`);
195
+ continue;
196
+ }
184
197
  try {
185
198
  const { readFileSync: readFileSync7, writeFileSync: writeFileSync6 } = await import('fs');
186
199
  let content;
187
200
  try {
188
- content = readFileSync7(file, "utf-8");
201
+ content = readFileSync7(safeFile, "utf-8");
189
202
  } catch {
190
- api.log.warn(`auto-doc: could not read file ${file}`);
203
+ api.log.warn(`auto-doc: could not read file ${safeFile}`);
191
204
  continue;
192
205
  }
193
206
  const entities = parseSource(content);
@@ -215,14 +228,14 @@ async function runAutoDoc(input, api) {
215
228
  }
216
229
  if (!doc) doc = generateDocComment(entity, includeTypes);
217
230
  modified = injectDocComment(modified, entity, doc);
218
- results.push({ file, entity: entity.name, source });
231
+ results.push({ file: safeFile, entity: entity.name, source });
219
232
  }
220
233
  if (!input.dry_run && results.length > 0) {
221
- writeFileSync6(file, modified, "utf-8");
222
- api.log.info(`auto-doc: updated ${file}`);
234
+ writeFileSync6(safeFile, modified, "utf-8");
235
+ api.log.info(`auto-doc: updated ${safeFile}`);
223
236
  }
224
237
  } catch (err) {
225
- api.log.error(`auto-doc: error processing ${file}: ${err}`);
238
+ api.log.error(`auto-doc: error processing ${safeFile}: ${err}`);
226
239
  }
227
240
  }
228
241
  return {
@@ -625,9 +638,21 @@ var plugin3 = {
625
638
  default: "block",
626
639
  description: '"block" refuses the call; "warn" injects context but lets it through.'
627
640
  },
628
- blockCommit: { type: "boolean", default: true, description: "Block commits on protected branches." },
629
- blockPush: { type: "boolean", default: true, description: "Block pushes from protected branches." },
630
- blockMerge: { type: "boolean", default: true, description: "Block merges into protected branches." }
641
+ blockCommit: {
642
+ type: "boolean",
643
+ default: true,
644
+ description: "Block commits on protected branches."
645
+ },
646
+ blockPush: {
647
+ type: "boolean",
648
+ default: true,
649
+ description: "Block pushes from protected branches."
650
+ },
651
+ blockMerge: {
652
+ type: "boolean",
653
+ default: true,
654
+ description: "Block merges into protected branches."
655
+ }
631
656
  }
632
657
  },
633
658
  setup(api) {
@@ -645,6 +670,7 @@ var plugin3 = {
645
670
  state3.invocationCount += 1;
646
671
  let gitOp = null;
647
672
  if (toolName === "git_autocommit") {
673
+ if (inp["dry_run"] === true) return;
648
674
  gitOp = { type: "commit", snippet: "git_autocommit" };
649
675
  } else if (toolName === "bash") {
650
676
  const command = inp["command"];
@@ -659,6 +685,7 @@ var plugin3 = {
659
685
  const when = (/* @__PURE__ */ new Date()).toISOString();
660
686
  const opVerb = gitOp.type === "commit" ? "committing to" : gitOp.type === "push" ? "pushing from" : "merging into";
661
687
  const hasUncommitted = detectUncommittedChanges(cwd);
688
+ const retryStep = toolName === "git_autocommit" ? "retry git_autocommit" : `git ${gitOp.type} ...`;
662
689
  const suggestionParts = [];
663
690
  if (hasUncommitted) {
664
691
  suggestionParts.push("git stash");
@@ -667,7 +694,7 @@ var plugin3 = {
667
694
  if (hasUncommitted) {
668
695
  suggestionParts.push("git stash pop");
669
696
  }
670
- suggestionParts.push(`git ${gitOp.type} ...`);
697
+ suggestionParts.push(retryStep);
671
698
  const suggestion = suggestionParts.join(" \u2192 ");
672
699
  const reason = `branch-guard: refused to ${gitOp.type} on protected branch '${branch}'. You're on a protected branch. Use a feature branch instead.
673
700
  ` + (hasUncommitted ? `You have uncommitted changes. Safe workflow:
@@ -769,12 +796,19 @@ var DEFAULTS2 = {
769
796
  collectCommits: true,
770
797
  maxEntries: 200
771
798
  };
799
+ function resolveProjectPath2(rawPath, cwd = process.cwd()) {
800
+ const root = resolve(cwd);
801
+ const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
802
+ const rel = relative(root, resolved);
803
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return resolved;
804
+ return null;
805
+ }
772
806
  function readConfig3(raw) {
773
807
  if (!raw || typeof raw !== "object") return { ...DEFAULTS2 };
774
808
  const r = raw;
775
809
  return {
776
810
  enabled: r["enabled"] !== false,
777
- filePath: typeof r["filePath"] === "string" && r["filePath"].length > 0 ? r["filePath"] : DEFAULTS2.filePath,
811
+ filePath: typeof r["filePath"] === "string" && r["filePath"].length > 0 ? resolveProjectPath2(r["filePath"]) ?? "" : resolveProjectPath2(DEFAULTS2.filePath) ?? DEFAULTS2.filePath,
778
812
  collectCommits: r["collectCommits"] !== false,
779
813
  maxEntries: typeof r["maxEntries"] === "number" && r["maxEntries"] >= 10 ? r["maxEntries"] : DEFAULTS2.maxEntries
780
814
  };
@@ -1044,6 +1078,9 @@ var plugin4 = {
1044
1078
  mutating: true,
1045
1079
  async execute(input) {
1046
1080
  if (!cfg.enabled) return { ok: false, error: "changelog-writer is disabled" };
1081
+ if (!cfg.filePath) {
1082
+ return { ok: false, error: "filePath must stay within the current project directory" };
1083
+ }
1047
1084
  if (state4.entries.length === 0) {
1048
1085
  return { ok: false, error: "no pending entries \u2014 add some with changelog_add first" };
1049
1086
  }
@@ -1142,6 +1179,13 @@ function readConfig4(raw) {
1142
1179
  maxFileBytes: typeof r["maxFileBytes"] === "number" && r["maxFileBytes"] >= 1024 ? r["maxFileBytes"] : DEFAULTS3.maxFileBytes
1143
1180
  };
1144
1181
  }
1182
+ function resolveProjectPath3(rawPath, cwd = process.cwd()) {
1183
+ const root = resolve(cwd);
1184
+ const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
1185
+ const rel = relative(root, resolved);
1186
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return resolved;
1187
+ return null;
1188
+ }
1145
1189
  function captureFile(path2, maxBytes) {
1146
1190
  try {
1147
1191
  const st = statSync(path2);
@@ -1208,7 +1252,9 @@ var plugin5 = {
1208
1252
  const ti = input.toolInput ?? {};
1209
1253
  const raw = ti["path"] ?? ti["file_path"] ?? ti["filePath"];
1210
1254
  if (typeof raw !== "string" || raw.length === 0) return;
1211
- const captured = captureFile(raw, cfg.maxFileBytes);
1255
+ const safePath = resolveProjectPath3(raw);
1256
+ if (!safePath) return;
1257
+ const captured = captureFile(safePath, cfg.maxFileBytes);
1212
1258
  if (captured === "too-large") {
1213
1259
  state5.skippedLarge += 1;
1214
1260
  return;
@@ -1250,9 +1296,15 @@ var plugin5 = {
1250
1296
  const paths = Array.isArray(input.paths) ? input.paths.filter((p) => typeof p === "string" && p.length > 0) : [];
1251
1297
  if (paths.length === 0) return { ok: false, error: "paths must not be empty" };
1252
1298
  const files = [];
1299
+ const rejectedOutsideProject = [];
1253
1300
  let skipped = 0;
1254
1301
  for (const p of paths) {
1255
- const captured = captureFile(p, cfg.maxFileBytes);
1302
+ const safePath = resolveProjectPath3(p);
1303
+ if (!safePath) {
1304
+ rejectedOutsideProject.push(p);
1305
+ continue;
1306
+ }
1307
+ const captured = captureFile(safePath, cfg.maxFileBytes);
1256
1308
  if (captured === "too-large") {
1257
1309
  skipped += 1;
1258
1310
  state5.skippedLarge += 1;
@@ -1260,6 +1312,13 @@ var plugin5 = {
1260
1312
  }
1261
1313
  files.push(captured);
1262
1314
  }
1315
+ if (rejectedOutsideProject.length > 0) {
1316
+ return {
1317
+ ok: false,
1318
+ error: "paths must stay within the current project directory",
1319
+ rejectedOutsideProject
1320
+ };
1321
+ }
1263
1322
  if (files.length === 0) {
1264
1323
  return { ok: false, error: "all files were skipped (too large)" };
1265
1324
  }
@@ -2087,12 +2146,21 @@ var DEFAULTS6 = {
2087
2146
  maxPins: 20,
2088
2147
  maxPinChars: 500
2089
2148
  };
2149
+ function resolveProjectPath4(rawPath, cwd = process.cwd()) {
2150
+ if (typeof rawPath !== "string" || rawPath.length === 0) return "";
2151
+ const root = resolve(cwd);
2152
+ const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
2153
+ const rel = relative(root, resolved);
2154
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return resolved;
2155
+ return null;
2156
+ }
2090
2157
  function readConfig7(raw) {
2091
2158
  if (!raw || typeof raw !== "object") return { ...DEFAULTS6 };
2092
2159
  const r = raw;
2160
+ const rawPath = typeof r["filePath"] === "string" ? r["filePath"] : DEFAULTS6.filePath;
2093
2161
  return {
2094
2162
  enabled: r["enabled"] !== false,
2095
- filePath: typeof r["filePath"] === "string" ? r["filePath"] : DEFAULTS6.filePath,
2163
+ filePath: rawPath ? resolveProjectPath4(rawPath) ?? "" : "",
2096
2164
  maxPins: typeof r["maxPins"] === "number" && r["maxPins"] >= 1 && r["maxPins"] <= 100 ? r["maxPins"] : DEFAULTS6.maxPins,
2097
2165
  maxPinChars: typeof r["maxPinChars"] === "number" && r["maxPinChars"] >= 20 ? r["maxPinChars"] : DEFAULTS6.maxPinChars
2098
2166
  };
@@ -4157,13 +4225,15 @@ var API_VERSION8 = "^0.1.10";
4157
4225
  var commitCount = { value: 0 };
4158
4226
  var lastCommit = { hash: null, at: null };
4159
4227
  var llmGenerated = { value: 0 };
4160
- function runGit(args, cwd) {
4228
+ var DEFAULT_GIT_TIMEOUT_MS = 3e4;
4229
+ var GIT_COMMIT_TIMEOUT_MS = 5 * 6e4;
4230
+ function runGit(args, cwd, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
4161
4231
  try {
4162
4232
  return execFileSync("git", args, {
4163
4233
  encoding: "utf-8",
4164
4234
  cwd,
4165
4235
  stdio: ["pipe", "pipe", "pipe"],
4166
- timeout: 3e4,
4236
+ timeout: timeoutMs,
4167
4237
  maxBuffer: 10 * 1024 * 1024,
4168
4238
  windowsHide: true
4169
4239
  }).trim();
@@ -4194,7 +4264,7 @@ function stageFiles(files, cwd) {
4194
4264
  runGit(["add", ...existing], cwd);
4195
4265
  }
4196
4266
  function commitWithMessage(message, cwd) {
4197
- return runGit(["commit", "-m", message], cwd);
4267
+ return runGit(["commit", "-m", message], cwd, GIT_COMMIT_TIMEOUT_MS);
4198
4268
  }
4199
4269
  function getWorktrees(cwd) {
4200
4270
  try {
@@ -4635,7 +4705,7 @@ function readConfig12(raw) {
4635
4705
  };
4636
4706
  }
4637
4707
  function runCommand(command, args, timeoutMs, cwd) {
4638
- return new Promise((resolve) => {
4708
+ return new Promise((resolve6) => {
4639
4709
  let timedOut = false;
4640
4710
  const stdoutChunks = [];
4641
4711
  const stderrChunks = [];
@@ -4647,17 +4717,17 @@ function runCommand(command, args, timeoutMs, cwd) {
4647
4717
  signal: AbortSignal.timeout(timeoutMs)
4648
4718
  });
4649
4719
  } catch {
4650
- resolve({ code: 127, stdout: "", stderr: "", timedOut: false });
4720
+ resolve6({ code: 127, stdout: "", stderr: "", timedOut: false });
4651
4721
  return;
4652
4722
  }
4653
4723
  child.stdout?.on("data", (c) => stdoutChunks.push(c));
4654
4724
  child.stderr?.on("data", (c) => stderrChunks.push(c));
4655
4725
  child.on("error", () => {
4656
- resolve({ code: 127, stdout: "", stderr: "", timedOut: false });
4726
+ resolve6({ code: 127, stdout: "", stderr: "", timedOut: false });
4657
4727
  });
4658
4728
  child.on("close", (code) => {
4659
4729
  if (timedOut) return;
4660
- resolve({
4730
+ resolve6({
4661
4731
  code,
4662
4732
  stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
4663
4733
  stderr: Buffer.concat(stderrChunks).toString("utf-8"),
@@ -4666,7 +4736,7 @@ function runCommand(command, args, timeoutMs, cwd) {
4666
4736
  });
4667
4737
  child.on("abort", () => {
4668
4738
  timedOut = true;
4669
- resolve({ code: null, stdout: "", stderr: "", timedOut: true });
4739
+ resolve6({ code: null, stdout: "", stderr: "", timedOut: true });
4670
4740
  });
4671
4741
  });
4672
4742
  }
@@ -6169,6 +6239,31 @@ var DEFAULTS17 = {
6169
6239
  timeoutMs: 5e3,
6170
6240
  maxConsecutiveFailures: 5
6171
6241
  };
6242
+ function isPrivateIPv4(hostname) {
6243
+ const parts = hostname.split(".").map((p) => Number(p));
6244
+ if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
6245
+ return false;
6246
+ }
6247
+ const [a, b] = parts;
6248
+ return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
6249
+ }
6250
+ function isBlockedHostname(hostname) {
6251
+ const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
6252
+ 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);
6253
+ }
6254
+ function normalizeWebhookUrl(raw) {
6255
+ if (typeof raw !== "string" || raw.trim().length === 0) return "";
6256
+ try {
6257
+ const url = new URL(raw.trim());
6258
+ if (url.protocol !== "https:" && url.protocol !== "http:") return "";
6259
+ if (url.username || url.password) return "";
6260
+ if (!url.hostname || isBlockedHostname(url.hostname)) return "";
6261
+ url.hash = "";
6262
+ return url.toString();
6263
+ } catch {
6264
+ return "";
6265
+ }
6266
+ }
6172
6267
  function readConfig18(raw) {
6173
6268
  if (!raw || typeof raw !== "object") return { ...DEFAULTS17, events: [...DEFAULTS17.events] };
6174
6269
  const r = raw;
@@ -6180,7 +6275,7 @@ function readConfig18(raw) {
6180
6275
  }
6181
6276
  return {
6182
6277
  enabled: r["enabled"] !== false,
6183
- webhookUrl: typeof r["webhookUrl"] === "string" ? r["webhookUrl"] : "",
6278
+ webhookUrl: normalizeWebhookUrl(r["webhookUrl"]),
6184
6279
  events: Array.isArray(r["events"]) ? r["events"].filter((e) => KNOWN_EVENTS.includes(e)) : [...DEFAULTS17.events],
6185
6280
  headers,
6186
6281
  timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] >= 500 && r["timeoutMs"] <= 6e4 ? r["timeoutMs"] : DEFAULTS17.timeoutMs,
@@ -7383,6 +7478,14 @@ var plugin26 = {
7383
7478
  };
7384
7479
  var secret_scanner_default = plugin26;
7385
7480
  var API_VERSION11 = "^0.1.10";
7481
+ function resolveProjectRoot(rawCwd, root = process.cwd()) {
7482
+ if (typeof rawCwd !== "string" || rawCwd.length === 0) return root;
7483
+ const base = resolve(root);
7484
+ const resolved = isAbsolute(rawCwd) ? resolve(rawCwd) : resolve(base, rawCwd);
7485
+ const rel = relative(base, resolved);
7486
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return resolved;
7487
+ return null;
7488
+ }
7386
7489
  var state24 = {
7387
7490
  /** Total invocations across all three tools this session. */
7388
7491
  invocationCount: 0,
@@ -7433,7 +7536,11 @@ function collectManifests(root) {
7433
7536
  function parseVersion(v) {
7434
7537
  const m = v.match(/^v?(\d+)\.(\d+)\.(\d+)/);
7435
7538
  if (!m) return [0, 0, 0];
7436
- return [Number.parseInt(expectDefined(m[1]), 10), Number.parseInt(expectDefined(m[2]), 10), Number.parseInt(expectDefined(m[3]), 10)];
7539
+ return [
7540
+ Number.parseInt(expectDefined(m[1]), 10),
7541
+ Number.parseInt(expectDefined(m[2]), 10),
7542
+ Number.parseInt(expectDefined(m[3]), 10)
7543
+ ];
7437
7544
  }
7438
7545
  function bumpVersion(version, part) {
7439
7546
  let [major, minor, patch] = parseVersion(version);
@@ -7569,6 +7676,11 @@ var plugin27 = {
7569
7676
  defaultPart = readDefaultPart(next);
7570
7677
  });
7571
7678
  async function performBump(part, dryRun, cwd) {
7679
+ const safeCwd = resolveProjectRoot(cwd);
7680
+ if (!safeCwd) {
7681
+ return { ok: false, error: "cwd must stay within the current project directory" };
7682
+ }
7683
+ cwd = safeCwd;
7572
7684
  const pkg = getPackageJson(cwd);
7573
7685
  if (!pkg) {
7574
7686
  return { ok: false, error: "No package.json found" };
@@ -7676,7 +7788,12 @@ var plugin27 = {
7676
7788
  properties: {
7677
7789
  cwd: { type: "string", description: "Working directory (defaults to project root)" },
7678
7790
  dry_run: { type: "boolean", default: false },
7679
- 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." }
7791
+ part: {
7792
+ type: "string",
7793
+ enum: ["major", "minor", "patch", "auto"],
7794
+ default: defaultPart,
7795
+ description: "Version part to bump. Omitted \u2192 the configured default (/settings semver-part, factory default: patch). Use auto to infer from commits."
7796
+ }
7680
7797
  }
7681
7798
  },
7682
7799
  permission: "confirm",
@@ -7740,7 +7857,11 @@ var plugin27 = {
7740
7857
  if (mode !== "patch" && mode !== "minor" && mode !== "major" && mode !== "auto") {
7741
7858
  return { message: `Unknown mode "${mode}". Use status, patch, minor, major or auto.` };
7742
7859
  }
7743
- const result = await performBump(mode, dry, cwd);
7860
+ const safeCwd = resolveProjectRoot(cwd);
7861
+ if (!safeCwd) {
7862
+ return { message: "cwd must stay within the current project directory" };
7863
+ }
7864
+ const result = await performBump(mode, dry, safeCwd);
7744
7865
  return { message: String(result["message"] ?? result["error"] ?? JSON.stringify(result)) };
7745
7866
  }
7746
7867
  });
@@ -7758,16 +7879,20 @@ var plugin27 = {
7758
7879
  async execute(input) {
7759
7880
  state24.invocationCount += 1;
7760
7881
  state24.perTool["semver_current"] = (state24.perTool["semver_current"] ?? 0) + 1;
7761
- const cwd = input["cwd"];
7762
- const pkg = getPackageJson(cwd);
7882
+ const cwdInput = input["cwd"];
7883
+ const safeCwd = resolveProjectRoot(cwdInput);
7884
+ if (!safeCwd) {
7885
+ return { ok: false, error: "cwd must stay within the current project directory" };
7886
+ }
7887
+ const pkg = getPackageJson(safeCwd);
7763
7888
  const currentVersion = pkg?.version ?? "unknown";
7764
7889
  let latestTag = null;
7765
7890
  let commitsSinceTag = 0;
7766
7891
  try {
7767
- const tagsOutput = runGit2(["describe", "--tags", "--abbrev=0"], cwd);
7892
+ const tagsOutput = runGit2(["describe", "--tags", "--abbrev=0"], safeCwd);
7768
7893
  latestTag = tagsOutput || null;
7769
7894
  if (latestTag) {
7770
- const countOutput = runGit2(["rev-list", "--count", `${latestTag}..HEAD`], cwd);
7895
+ const countOutput = runGit2(["rev-list", "--count", `${latestTag}..HEAD`], safeCwd);
7771
7896
  commitsSinceTag = Number.parseInt(countOutput, 10) || 0;
7772
7897
  }
7773
7898
  } catch {
@@ -7802,11 +7927,15 @@ var plugin27 = {
7802
7927
  const from = input["from"];
7803
7928
  const to = input["to"] ?? "HEAD";
7804
7929
  const cwd = input["cwd"];
7930
+ const safeCwd = resolveProjectRoot(cwd);
7931
+ if (!safeCwd) {
7932
+ return { ok: false, error: "cwd must stay within the current project directory" };
7933
+ }
7805
7934
  const format = input["format"] ?? "markdown";
7806
7935
  const range = from ? `${from}..${to}` : to;
7807
7936
  let commits;
7808
7937
  try {
7809
- const output = runGit2(["log", range === to ? "-30" : range, "--format=%H %s"], cwd);
7938
+ const output = runGit2(["log", range === to ? "-30" : range, "--format=%H %s"], safeCwd);
7810
7939
  commits = output.split("\n").filter(Boolean).map((line) => {
7811
7940
  const spaceIdx = line.indexOf(" ");
7812
7941
  const hash = line.slice(0, spaceIdx);
@@ -8530,27 +8659,21 @@ function expandTemplate(template, variables) {
8530
8659
  return result;
8531
8660
  }
8532
8661
  function expandConditionals(template, variables) {
8533
- return template.replace(
8534
- /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
8535
- (_, key, content) => {
8536
- const val = variables[key];
8537
- return val !== void 0 && val !== "" && val !== "false" && val !== "0" ? content : "";
8538
- }
8539
- );
8662
+ return template.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, content) => {
8663
+ const val = variables[key];
8664
+ return val !== void 0 && val !== "" && val !== "false" && val !== "0" ? content : "";
8665
+ });
8540
8666
  }
8541
8667
  function expandLoops(template, variables) {
8542
- return template.replace(
8543
- /\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
8544
- (_, key, content) => {
8545
- const val = variables[key];
8546
- if (!val) return "";
8547
- if (typeof val === "string" && val.includes(",")) {
8548
- const items = val.split(",").map((s) => s.trim());
8549
- return items.map((item) => expandTemplate(content, { ...variables, [key]: item })).join("\n");
8550
- }
8551
- return expandTemplate(content, variables);
8552
- }
8553
- );
8668
+ return template.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, key, content) => {
8669
+ const val = variables[key];
8670
+ if (!val) return "";
8671
+ if (typeof val === "string" && val.includes(",")) {
8672
+ const items = val.split(",").map((s) => s.trim());
8673
+ return items.map((item) => expandTemplate(content, { ...variables, [key]: item })).join("\n");
8674
+ }
8675
+ return expandTemplate(content, variables);
8676
+ });
8554
8677
  }
8555
8678
  function renderTemplate(template, variables, escapeHtml = true) {
8556
8679
  let result = template;
@@ -8569,6 +8692,12 @@ function renderTemplateRaw(template, variables) {
8569
8692
  result = expandTemplate(result, variables);
8570
8693
  return result;
8571
8694
  }
8695
+ function validateRelativeTemplatePath(field, value) {
8696
+ if (isAbsolute(value) || value.split(/[\\/]+/).includes("..")) {
8697
+ return `${field} must be a relative path without ".." components`;
8698
+ }
8699
+ return null;
8700
+ }
8572
8701
  var plugin30 = {
8573
8702
  name: "template-engine",
8574
8703
  version: "0.1.0",
@@ -8597,13 +8726,19 @@ var plugin30 = {
8597
8726
  inputSchema: {
8598
8727
  type: "object",
8599
8728
  properties: {
8600
- template: { type: "string", description: "Template string with {{variable}} placeholders" },
8729
+ template: {
8730
+ type: "string",
8731
+ description: "Template string with {{variable}} placeholders"
8732
+ },
8601
8733
  variables: {
8602
8734
  type: "object",
8603
8735
  description: "Variables to substitute into the template",
8604
8736
  additionalProperties: { type: "string" }
8605
8737
  },
8606
- output_path: { type: "string", description: "Optional path to write the expanded result" },
8738
+ output_path: {
8739
+ type: "string",
8740
+ description: "Optional path to write the expanded result"
8741
+ },
8607
8742
  raw: { type: "boolean", default: false, description: "Disable HTML auto-escaping" }
8608
8743
  },
8609
8744
  required: ["template", "variables"]
@@ -8629,9 +8764,8 @@ var plugin30 = {
8629
8764
  return { ok: false, error: String(err) };
8630
8765
  }
8631
8766
  if (output_path) {
8632
- if (isAbsolute(output_path) || output_path.includes("..")) {
8633
- return { ok: false, error: 'output_path must be a relative path without ".." components' };
8634
- }
8767
+ const pathError = validateRelativeTemplatePath("output_path", output_path);
8768
+ if (pathError) return { ok: false, error: pathError };
8635
8769
  const { writeFileSync: writeFileSync6 } = await import('fs');
8636
8770
  writeFileSync6(output_path, result, "utf-8");
8637
8771
  return {
@@ -8661,7 +8795,10 @@ var plugin30 = {
8661
8795
  description: "Variables to substitute",
8662
8796
  additionalProperties: { type: "string" }
8663
8797
  },
8664
- output_path: { type: "string", description: "Optional path to write the rendered result" },
8798
+ output_path: {
8799
+ type: "string",
8800
+ description: "Optional path to write the rendered result"
8801
+ },
8665
8802
  raw: { type: "boolean", default: false }
8666
8803
  },
8667
8804
  required: ["template_path", "variables"]
@@ -8676,6 +8813,8 @@ var plugin30 = {
8676
8813
  if (!template_path || typeof template_path !== "string") {
8677
8814
  return { ok: false, error: "template_path is required and must be a string" };
8678
8815
  }
8816
+ const templatePathError = validateRelativeTemplatePath("template_path", template_path);
8817
+ if (templatePathError) return { ok: false, error: templatePathError };
8679
8818
  if (!variables || typeof variables !== "object") {
8680
8819
  return { ok: false, error: "variables is required and must be an object" };
8681
8820
  }
@@ -8693,9 +8832,8 @@ var plugin30 = {
8693
8832
  return { ok: false, error: `Template rendering failed: ${err}` };
8694
8833
  }
8695
8834
  if (output_path) {
8696
- if (isAbsolute(output_path) || output_path.includes("..")) {
8697
- return { ok: false, error: 'output_path must be a relative path without ".." components' };
8698
- }
8835
+ const pathError = validateRelativeTemplatePath("output_path", output_path);
8836
+ if (pathError) return { ok: false, error: pathError };
8699
8837
  const { writeFileSync: writeFileSync6 } = await import('fs');
8700
8838
  writeFileSync6(output_path, result, "utf-8");
8701
8839
  return {
@@ -8720,8 +8858,14 @@ var plugin30 = {
8720
8858
  type: "object",
8721
8859
  properties: {
8722
8860
  name: { type: "string", description: "Unique name for this template" },
8723
- content: { type: "string", description: "Template content with {{variable}} placeholders" },
8724
- description: { type: "string", description: "Optional description of what this template is for" }
8861
+ content: {
8862
+ type: "string",
8863
+ description: "Template content with {{variable}} placeholders"
8864
+ },
8865
+ description: {
8866
+ type: "string",
8867
+ description: "Optional description of what this template is for"
8868
+ }
8725
8869
  },
8726
8870
  required: ["name", "content"]
8727
8871
  },
@@ -10123,7 +10267,7 @@ function estimateRequestTokens(request, charsPerToken) {
10123
10267
  walk(request["messages"]);
10124
10268
  return Math.ceil(chars / charsPerToken);
10125
10269
  }
10126
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10270
+ var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
10127
10271
  var plugin35 = {
10128
10272
  name: "token-throttle",
10129
10273
  version: "0.1.0",
@@ -10380,9 +10524,26 @@ function isWrappedAsLinkOrCode(line, name) {
10380
10524
  function escapeRegExp(s) {
10381
10525
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10382
10526
  }
10527
+ function mapMarkdownFences(lines) {
10528
+ const fenced = new Array(lines.length).fill(false);
10529
+ let inFence = false;
10530
+ for (let i = 0; i < lines.length; i++) {
10531
+ const line = lines[i];
10532
+ if (/^\s*```/.test(line)) {
10533
+ fenced[i] = true;
10534
+ inFence = !inFence;
10535
+ continue;
10536
+ }
10537
+ fenced[i] = inFence;
10538
+ }
10539
+ return fenced;
10540
+ }
10383
10541
  function findUnlinkedReferences(lines, names) {
10384
10542
  const found = /* @__PURE__ */ new Map();
10385
- for (const line of lines) {
10543
+ const fencedLines = mapMarkdownFences(lines);
10544
+ for (let i = 0; i < lines.length; i++) {
10545
+ if (fencedLines[i]) continue;
10546
+ const line = lines[i];
10386
10547
  if (line.length === 0) continue;
10387
10548
  for (const name of names) {
10388
10549
  const re = new RegExp(`(^|[^\\w-])${escapeRegExp(name)}(?![\\w-])`, "i");
@@ -10395,8 +10556,10 @@ function findUnlinkedReferences(lines, names) {
10395
10556
  }
10396
10557
  function wrapUnlinkedReferences(content) {
10397
10558
  const lines = content.split("\n");
10559
+ const fencedLines = mapMarkdownFences(lines);
10398
10560
  let changed = false;
10399
10561
  for (let i = 0; i < lines.length; i++) {
10562
+ if (fencedLines[i]) continue;
10400
10563
  const line = lines[i];
10401
10564
  if (line.length === 0) continue;
10402
10565
  const newLine = wrapLineReferences(line);
@@ -10512,7 +10675,9 @@ var plugin36 = {
10512
10675
  state32.unlinkedCount += 1;
10513
10676
  const limited = unlinked.slice(0, cfg.maxReferences);
10514
10677
  const overflow = unlinked.length - limited.length;
10515
- const lines = limited.map((name) => `- \`${name}\` \u2192 \`[${name}](${PLUGIN_CATALOG.get(name) ?? `./src/${name}`})\``).join("\n");
10678
+ const lines = limited.map(
10679
+ (name) => `- \`${name}\` \u2192 \`[${name}](${PLUGIN_CATALOG.get(name) ?? `./src/${name}`})\``
10680
+ ).join("\n");
10516
10681
  const overflowNote = overflow > 0 ? `
10517
10682
  - \u2026and ${overflow} more` : "";
10518
10683
  return {