@wrongstack/plugins 0.1.0 → 0.9.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.
package/dist/auto-doc.js CHANGED
@@ -45,7 +45,6 @@ function parseSource(content) {
45
45
  m = line.match(reInterface);
46
46
  if (m) {
47
47
  entities.push({ kind: "interface", name: m[1], startLine: i + 1 });
48
- continue;
49
48
  }
50
49
  }
51
50
  return entities;
@@ -68,7 +68,10 @@ var plugin = {
68
68
  sessionCost.totalCompletionTokens += completionTokens;
69
69
  sessionCost.totalTokens += totalTokens;
70
70
  sessionCost.totalCostUsd += costUsd;
71
- const slot = sessionCost.byModel[model] ??= { tokens: 0, costUsd: 0, requests: 0 };
71
+ if (sessionCost.byModel[model] === void 0) {
72
+ sessionCost.byModel[model] = { tokens: 0, costUsd: 0, requests: 0 };
73
+ }
74
+ const slot = sessionCost.byModel[model];
72
75
  slot.tokens += totalTokens;
73
76
  slot.costUsd += costUsd;
74
77
  slot.requests += 1;
package/dist/cron.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/cron/index.ts
2
2
  var API_VERSION = "^0.1.10";
3
3
  function formatNextRun(intervalMs) {
4
- const ms = isNaN(intervalMs) || intervalMs <= 0 ? 6e4 : intervalMs;
4
+ const ms = Number.isNaN(intervalMs) || intervalMs <= 0 ? 6e4 : intervalMs;
5
5
  return new Date(Date.now() + ms).toISOString();
6
6
  }
7
7
  var plugin = {
@@ -124,7 +124,7 @@ var plugin = {
124
124
  if (!name || typeof name !== "string" || name.trim() === "") {
125
125
  return { ok: false, error: "name is required and must be a non-empty string" };
126
126
  }
127
- if (isNaN(intervalMs)) {
127
+ if (Number.isNaN(intervalMs)) {
128
128
  return { ok: false, error: "intervalMs must be a number >= 1000" };
129
129
  }
130
130
  if (state.jobs.has(name)) {
@@ -1,4 +1,5 @@
1
1
  import { watch } from 'fs';
2
+ import * as path from 'path';
2
3
 
3
4
  // src/file-watcher/index.ts
4
5
  var API_VERSION = "^0.1.10";
@@ -15,19 +16,31 @@ var plugin = {
15
16
  defaultConfig: {
16
17
  debounceMs: 500,
17
18
  watchOnStartup: [],
18
- autoUnwatchOnExit: true
19
+ autoUnwatchOnExit: true,
20
+ autoIndex: false,
21
+ indexProjectRoot: ""
19
22
  },
20
23
  configSchema: {
21
24
  type: "object",
22
25
  properties: {
23
26
  debounceMs: { type: "number", default: 500 },
24
27
  watchOnStartup: { type: "array", items: { type: "string" }, default: [] },
25
- autoUnwatchOnExit: { type: "boolean", default: true }
28
+ autoUnwatchOnExit: { type: "boolean", default: true },
29
+ autoIndex: {
30
+ type: "boolean",
31
+ default: false,
32
+ description: "When true, automatically reindex changed .ts/.tsx/.js/.jsx files via codebase-index (incremental)"
33
+ },
34
+ indexProjectRoot: {
35
+ type: "string",
36
+ default: "",
37
+ description: "Project root directory for the indexer. Defaults to cwd when empty."
38
+ }
26
39
  }
27
40
  },
28
41
  setup(api) {
29
42
  const watches = /* @__PURE__ */ new Map();
30
- let debounceMs = api.config.extensions?.["file-watcher"]?.["debounceMs"] ?? 500;
43
+ const debounceMs = api.config.extensions?.["file-watcher"]?.["debounceMs"] ?? 500;
31
44
  const debounceTimers = /* @__PURE__ */ new Map();
32
45
  function debounceEvent(key, fn, ms) {
33
46
  const existing = debounceTimers.get(key);
@@ -37,6 +50,12 @@ var plugin = {
37
50
  fn();
38
51
  }, ms));
39
52
  }
53
+ const autoIndex = api.config.extensions?.["file-watcher"]?.["autoIndex"] ?? false;
54
+ const indexProjectRoot = api.config.extensions?.["file-watcher"]?.["indexProjectRoot"] ?? "";
55
+ const INDEXABLE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
56
+ function isIndexableFile(filePath) {
57
+ return INDEXABLE_EXTENSIONS.has(path.extname(filePath));
58
+ }
40
59
  function safeWatchDir(dirPath, recursive, handle) {
41
60
  try {
42
61
  const watcher = watch(dirPath, { recursive }, (eventType, filename) => {
@@ -53,6 +72,34 @@ var plugin = {
53
72
  });
54
73
  api.metrics.counter("file_change", 1, { event: eventType ?? "unknown" });
55
74
  api.log.debug(`file-watcher: ${eventType} ${fullPath} (watch=${handle.id})`);
75
+ if (autoIndex && isIndexableFile(fullPath)) {
76
+ debounceEvent(`index:${fullPath}`, async () => {
77
+ try {
78
+ const { runIndexer } = await import('@wrongstack/tools/codebase-index/index.js');
79
+ const root = indexProjectRoot || dirPath;
80
+ const fakeAppend = async () => {
81
+ };
82
+ const fakeClose = async () => {
83
+ };
84
+ const fakeRecordFileChange = () => {
85
+ };
86
+ const ctx = {
87
+ projectRoot: root,
88
+ cwd: root,
89
+ messages: [],
90
+ todos: [],
91
+ readFiles: /* @__PURE__ */ new Set(),
92
+ fileMtimes: /* @__PURE__ */ new Map(),
93
+ session: { id: "fw", append: fakeAppend, close: fakeClose, recordFileChange: fakeRecordFileChange }
94
+ };
95
+ await runIndexer(ctx, { projectRoot: root, files: [fullPath] });
96
+ api.metrics.counter("index_file", 1);
97
+ api.log.debug(`file-watcher: auto-index triggered for ${fullPath}`);
98
+ } catch (err) {
99
+ api.log.warn(`file-watcher: auto-index failed for ${fullPath}: ${err}`);
100
+ }
101
+ }, debounceMs);
102
+ }
56
103
  }, debounceMs);
57
104
  });
58
105
  watcher.on("error", (err) => {
@@ -1,11 +1,11 @@
1
- import { execSync } from 'child_process';
1
+ import { execFileSync } from 'child_process';
2
2
  import { existsSync } from 'fs';
3
3
 
4
4
  // src/git-autocommit/index.ts
5
5
  var API_VERSION = "^0.1.10";
6
6
  function runGit(args, cwd) {
7
7
  try {
8
- return execSync(`git ${args.join(" ")}`, {
8
+ return execFileSync("git", args, {
9
9
  encoding: "utf-8",
10
10
  cwd,
11
11
  stdio: ["pipe", "pipe", "pipe"],
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
- import { execSync } from 'child_process';
1
+ import { execFileSync, execSync } from 'child_process';
2
2
  import { watch, existsSync, readdirSync, readFileSync } from 'fs';
3
- import { join } from 'path';
3
+ import * as path from 'path';
4
+ import { isAbsolute, join } from 'path';
5
+ import { lookup } from 'dns/promises';
6
+ import { isIPv4, isIPv6 } from 'net';
4
7
 
5
8
  // src/auto-doc/index.ts
6
9
  var AUTO_DOC_API_VERSION = "^0.1.10";
@@ -49,7 +52,6 @@ function parseSource(content) {
49
52
  m = line.match(reInterface);
50
53
  if (m) {
51
54
  entities.push({ kind: "interface", name: m[1], startLine: i + 1 });
52
- continue;
53
55
  }
54
56
  }
55
57
  return entities;
@@ -242,7 +244,7 @@ var auto_doc_default = plugin;
242
244
  var API_VERSION = "^0.1.10";
243
245
  function runGit(args, cwd) {
244
246
  try {
245
- return execSync(`git ${args.join(" ")}`, {
247
+ return execFileSync("git", args, {
246
248
  encoding: "utf-8",
247
249
  cwd,
248
250
  stdio: ["pipe", "pipe", "pipe"],
@@ -562,7 +564,7 @@ function runShellCheck(files, severity, cwd) {
562
564
  ];
563
565
  let raw;
564
566
  try {
565
- raw = execSync(`shellcheck ${args.join(" ")}`, {
567
+ raw = execFileSync("shellcheck", args, {
566
568
  encoding: "utf-8",
567
569
  cwd,
568
570
  stdio: ["pipe", "pipe", "pipe"],
@@ -657,7 +659,7 @@ var plugin3 = {
657
659
  required: ["files"]
658
660
  },
659
661
  permission: "auto",
660
- mutating: false,
662
+ mutating: true,
661
663
  async execute(input) {
662
664
  const files = input["files"];
663
665
  const severity = input["severity"] ?? "warning";
@@ -670,7 +672,10 @@ var plugin3 = {
670
672
  }
671
673
  const byFile = {};
672
674
  for (const issue of issues) {
673
- (byFile[issue.file] ??= []).push(issue);
675
+ if (byFile[issue.file] === void 0) {
676
+ byFile[issue.file] = [];
677
+ }
678
+ byFile[issue.file].push(issue);
674
679
  }
675
680
  const errorCount = issues.filter((i) => i.level === "error").length;
676
681
  const warningCount = issues.filter((i) => i.level === "warning").length;
@@ -718,7 +723,7 @@ var plugin3 = {
718
723
  }
719
724
  },
720
725
  permission: "auto",
721
- mutating: false,
726
+ mutating: true,
722
727
  async execute(input) {
723
728
  const dir = input["directory"] ?? ".";
724
729
  const pattern = input["pattern"] ?? "";
@@ -736,7 +741,10 @@ var plugin3 = {
736
741
  }
737
742
  const byFile = {};
738
743
  for (const issue of issues) {
739
- (byFile[issue.file] ??= []).push(issue);
744
+ if (byFile[issue.file] === void 0) {
745
+ byFile[issue.file] = [];
746
+ }
747
+ byFile[issue.file].push(issue);
740
748
  }
741
749
  return {
742
750
  ok: true,
@@ -827,7 +835,10 @@ var plugin4 = {
827
835
  sessionCost.totalCompletionTokens += completionTokens;
828
836
  sessionCost.totalTokens += totalTokens;
829
837
  sessionCost.totalCostUsd += costUsd;
830
- const slot = sessionCost.byModel[model] ??= { tokens: 0, costUsd: 0, requests: 0 };
838
+ if (sessionCost.byModel[model] === void 0) {
839
+ sessionCost.byModel[model] = { tokens: 0, costUsd: 0, requests: 0 };
840
+ }
841
+ const slot = sessionCost.byModel[model];
831
842
  slot.tokens += totalTokens;
832
843
  slot.costUsd += costUsd;
833
844
  slot.requests += 1;
@@ -978,19 +989,31 @@ var plugin5 = {
978
989
  defaultConfig: {
979
990
  debounceMs: 500,
980
991
  watchOnStartup: [],
981
- autoUnwatchOnExit: true
992
+ autoUnwatchOnExit: true,
993
+ autoIndex: false,
994
+ indexProjectRoot: ""
982
995
  },
983
996
  configSchema: {
984
997
  type: "object",
985
998
  properties: {
986
999
  debounceMs: { type: "number", default: 500 },
987
1000
  watchOnStartup: { type: "array", items: { type: "string" }, default: [] },
988
- autoUnwatchOnExit: { type: "boolean", default: true }
1001
+ autoUnwatchOnExit: { type: "boolean", default: true },
1002
+ autoIndex: {
1003
+ type: "boolean",
1004
+ default: false,
1005
+ description: "When true, automatically reindex changed .ts/.tsx/.js/.jsx files via codebase-index (incremental)"
1006
+ },
1007
+ indexProjectRoot: {
1008
+ type: "string",
1009
+ default: "",
1010
+ description: "Project root directory for the indexer. Defaults to cwd when empty."
1011
+ }
989
1012
  }
990
1013
  },
991
1014
  setup(api) {
992
1015
  const watches = /* @__PURE__ */ new Map();
993
- let debounceMs = api.config.extensions?.["file-watcher"]?.["debounceMs"] ?? 500;
1016
+ const debounceMs = api.config.extensions?.["file-watcher"]?.["debounceMs"] ?? 500;
994
1017
  const debounceTimers = /* @__PURE__ */ new Map();
995
1018
  function debounceEvent(key, fn, ms) {
996
1019
  const existing = debounceTimers.get(key);
@@ -1000,6 +1023,12 @@ var plugin5 = {
1000
1023
  fn();
1001
1024
  }, ms));
1002
1025
  }
1026
+ const autoIndex = api.config.extensions?.["file-watcher"]?.["autoIndex"] ?? false;
1027
+ const indexProjectRoot = api.config.extensions?.["file-watcher"]?.["indexProjectRoot"] ?? "";
1028
+ const INDEXABLE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
1029
+ function isIndexableFile(filePath) {
1030
+ return INDEXABLE_EXTENSIONS.has(path.extname(filePath));
1031
+ }
1003
1032
  function safeWatchDir(dirPath, recursive, handle) {
1004
1033
  try {
1005
1034
  const watcher = watch(dirPath, { recursive }, (eventType, filename) => {
@@ -1016,6 +1045,34 @@ var plugin5 = {
1016
1045
  });
1017
1046
  api.metrics.counter("file_change", 1, { event: eventType ?? "unknown" });
1018
1047
  api.log.debug(`file-watcher: ${eventType} ${fullPath} (watch=${handle.id})`);
1048
+ if (autoIndex && isIndexableFile(fullPath)) {
1049
+ debounceEvent(`index:${fullPath}`, async () => {
1050
+ try {
1051
+ const { runIndexer } = await import('@wrongstack/tools/codebase-index/index.js');
1052
+ const root = indexProjectRoot || dirPath;
1053
+ const fakeAppend = async () => {
1054
+ };
1055
+ const fakeClose = async () => {
1056
+ };
1057
+ const fakeRecordFileChange = () => {
1058
+ };
1059
+ const ctx = {
1060
+ projectRoot: root,
1061
+ cwd: root,
1062
+ messages: [],
1063
+ todos: [],
1064
+ readFiles: /* @__PURE__ */ new Set(),
1065
+ fileMtimes: /* @__PURE__ */ new Map(),
1066
+ session: { id: "fw", append: fakeAppend, close: fakeClose, recordFileChange: fakeRecordFileChange }
1067
+ };
1068
+ await runIndexer(ctx, { projectRoot: root, files: [fullPath] });
1069
+ api.metrics.counter("index_file", 1);
1070
+ api.log.debug(`file-watcher: auto-index triggered for ${fullPath}`);
1071
+ } catch (err) {
1072
+ api.log.warn(`file-watcher: auto-index failed for ${fullPath}: ${err}`);
1073
+ }
1074
+ }, debounceMs);
1075
+ }
1019
1076
  }, debounceMs);
1020
1077
  });
1021
1078
  watcher.on("error", (err) => {
@@ -1151,8 +1208,6 @@ var plugin5 = {
1151
1208
  }
1152
1209
  };
1153
1210
  var file_watcher_default = plugin5;
1154
-
1155
- // src/web-search/index.ts
1156
1211
  var API_VERSION5 = "^0.1.10";
1157
1212
  async function duckduckgoSearch(query, numResults) {
1158
1213
  const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}&kl=us-en`;
@@ -1182,13 +1237,81 @@ async function duckduckgoSearch(query, numResults) {
1182
1237
  }
1183
1238
  return results;
1184
1239
  }
1240
+ function isPrivateIPv4(host) {
1241
+ const parts = host.split(".").map(Number);
1242
+ if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return true;
1243
+ const [a, b] = parts;
1244
+ return a === 0 || // 0.0.0.0/8 "this host"
1245
+ a === 10 || // private
1246
+ a === 127 || // loopback
1247
+ a === 100 && b >= 64 && b <= 127 || // CGNAT 100.64/10
1248
+ a === 169 && b === 254 || // link-local incl. 169.254.169.254 (cloud IMDS)
1249
+ a === 172 && b >= 16 && b <= 31 || // private
1250
+ a === 192 && b === 168 || // private
1251
+ a >= 224;
1252
+ }
1253
+ function isPrivateIPv6(raw) {
1254
+ const host = raw.toLowerCase();
1255
+ if (host === "::1" || host === "::") return true;
1256
+ const mapped = host.match(/(?:::ffff:)?(\d+\.\d+\.\d+\.\d+)$/);
1257
+ if (mapped?.[1]) return isPrivateIPv4(mapped[1]);
1258
+ if (host.startsWith("fc") || host.startsWith("fd")) return true;
1259
+ if (host.startsWith("fe8") || host.startsWith("fe9") || host.startsWith("fea") || host.startsWith("feb"))
1260
+ return true;
1261
+ return false;
1262
+ }
1263
+ function assertSafeIp(ip) {
1264
+ if (isIPv4(ip) && isPrivateIPv4(ip)) {
1265
+ throw new Error(`Blocked private/loopback address: ${ip}`);
1266
+ }
1267
+ if (isIPv6(ip) && isPrivateIPv6(ip)) {
1268
+ throw new Error(`Blocked private/loopback address: ${ip}`);
1269
+ }
1270
+ }
1271
+ async function assertSafeUrl(rawUrl) {
1272
+ const u = new URL(rawUrl);
1273
+ if (u.protocol !== "http:" && u.protocol !== "https:") {
1274
+ throw new Error(`Unsupported protocol: ${u.protocol}`);
1275
+ }
1276
+ const host = u.hostname.startsWith("[") && u.hostname.endsWith("]") ? u.hostname.slice(1, -1) : u.hostname;
1277
+ if (host === "localhost" || host.endsWith(".localhost") || host === "" || host === "0.0.0.0") {
1278
+ throw new Error("Blocked localhost target");
1279
+ }
1280
+ if (isIPv4(host) || isIPv6(host)) {
1281
+ assertSafeIp(host);
1282
+ return;
1283
+ }
1284
+ let addrs;
1285
+ try {
1286
+ addrs = await lookup(host, { all: true });
1287
+ } catch {
1288
+ throw new Error(`Could not resolve host: ${host}`);
1289
+ }
1290
+ for (const { address } of addrs) assertSafeIp(address);
1291
+ }
1185
1292
  async function fetchUrl(url, format) {
1186
- const resp = await fetch(url, {
1187
- headers: {
1188
- "User-Agent": "Mozilla/5.0 (compatible; WrongStack/1.0; +https://wrongstack.com)",
1189
- "Accept": format === "text" ? "text/plain" : "text/html"
1293
+ const MAX_REDIRECTS = 5;
1294
+ let currentUrl = url;
1295
+ let resp;
1296
+ for (let i = 0; i <= MAX_REDIRECTS; i++) {
1297
+ await assertSafeUrl(currentUrl);
1298
+ resp = await fetch(currentUrl, {
1299
+ redirect: "manual",
1300
+ headers: {
1301
+ "User-Agent": "Mozilla/5.0 (compatible; WrongStack/1.0; +https://wrongstack.com)",
1302
+ Accept: format === "text" ? "text/plain" : "text/html"
1303
+ }
1304
+ });
1305
+ if (resp.status >= 300 && resp.status < 400) {
1306
+ const loc = resp.headers.get("location");
1307
+ if (!loc) break;
1308
+ currentUrl = new URL(loc, currentUrl).toString();
1309
+ if (i === MAX_REDIRECTS) throw new Error("Too many redirects");
1310
+ continue;
1190
1311
  }
1191
- });
1312
+ break;
1313
+ }
1314
+ if (!resp) throw new Error(`Failed to fetch ${url}`);
1192
1315
  if (!resp.ok) throw new Error(`Failed to fetch ${url}: ${resp.status} ${resp.statusText}`);
1193
1316
  if (format === "text") {
1194
1317
  return resp.text();
@@ -1262,7 +1385,7 @@ var plugin6 = {
1262
1385
  required: ["query"]
1263
1386
  },
1264
1387
  permission: "auto",
1265
- mutating: false,
1388
+ mutating: true,
1266
1389
  async execute(input) {
1267
1390
  const query = input["query"];
1268
1391
  if (!query || typeof query !== "string" || query.trim() === "") {
@@ -1376,7 +1499,7 @@ function jmespathSearch(data, query) {
1376
1499
  }
1377
1500
  const arrMatch = query.match(/^\[(\d+)\](?:\.(.+))?$/);
1378
1501
  if (arrMatch) {
1379
- const idx = parseInt(arrMatch[1], 10);
1502
+ const idx = Number.parseInt(arrMatch[1], 10);
1380
1503
  const rest = arrMatch[2];
1381
1504
  const arr = data;
1382
1505
  const val = arr?.[idx];
@@ -1411,9 +1534,9 @@ function jmespathSearch(data, query) {
1411
1534
  const itemVal = item[field];
1412
1535
  switch (op) {
1413
1536
  case "==":
1414
- return itemVal == cmpVal;
1537
+ return itemVal === cmpVal;
1415
1538
  case "!=":
1416
- return itemVal != cmpVal;
1539
+ return itemVal !== cmpVal;
1417
1540
  case ">":
1418
1541
  return Number(itemVal) > Number(cmpVal);
1419
1542
  case "<":
@@ -1456,46 +1579,46 @@ function jmespathSearch(data, query) {
1456
1579
  }
1457
1580
  function validateJsonSchema(data, schema) {
1458
1581
  const errors = [];
1459
- function check(value, s, path) {
1582
+ function check(value, s, path2) {
1460
1583
  if (s["type"]) {
1461
1584
  const expectedType = s["type"];
1462
1585
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
1463
1586
  if (expectedType === "integer") {
1464
- if (!Number.isInteger(value)) errors.push(`${path}: expected integer, got ${actualType}`);
1587
+ if (!Number.isInteger(value)) errors.push(`${path2}: expected integer, got ${actualType}`);
1465
1588
  } else if (expectedType !== actualType) {
1466
- errors.push(`${path}: expected ${expectedType}, got ${actualType}`);
1589
+ errors.push(`${path2}: expected ${expectedType}, got ${actualType}`);
1467
1590
  }
1468
1591
  }
1469
1592
  if (typeof value === "string" && s["format"] === "uri" && value) {
1470
1593
  try {
1471
1594
  new URL(value);
1472
1595
  } catch {
1473
- errors.push(`${path}: not a valid URI`);
1596
+ errors.push(`${path2}: not a valid URI`);
1474
1597
  }
1475
1598
  }
1476
1599
  if (typeof value === "string" && s["pattern"]) {
1477
1600
  const re = new RegExp(s["pattern"]);
1478
- if (!re.test(value)) errors.push(`${path}: does not match pattern ${s["pattern"]}`);
1601
+ if (!re.test(value)) errors.push(`${path2}: does not match pattern ${s["pattern"]}`);
1479
1602
  }
1480
1603
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
1481
- errors.push(`${path}: string too short (min ${s["minLength"]})`);
1604
+ errors.push(`${path2}: string too short (min ${s["minLength"]})`);
1482
1605
  }
1483
1606
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
1484
- errors.push(`${path}: string too long (max ${s["maxLength"]})`);
1607
+ errors.push(`${path2}: string too long (max ${s["maxLength"]})`);
1485
1608
  }
1486
1609
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
1487
- errors.push(`${path}: below minimum ${s["minimum"]}`);
1610
+ errors.push(`${path2}: below minimum ${s["minimum"]}`);
1488
1611
  }
1489
1612
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
1490
- errors.push(`${path}: above maximum ${s["maximum"]}`);
1613
+ errors.push(`${path2}: above maximum ${s["maximum"]}`);
1491
1614
  }
1492
1615
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
1493
- value.forEach((item, i) => check(item, s["items"], `${path}[${i}]`));
1616
+ value.forEach((item, i) => check(item, s["items"], `${path2}[${i}]`));
1494
1617
  }
1495
1618
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
1496
1619
  const props = s["properties"];
1497
1620
  for (const [k, propSchema] of Object.entries(props)) {
1498
- check(value[k], propSchema, `${path}.${k}`);
1621
+ check(value[k], propSchema, `${path2}.${k}`);
1499
1622
  }
1500
1623
  }
1501
1624
  }
@@ -1674,7 +1797,7 @@ var json_path_default = plugin7;
1674
1797
  // src/cron/index.ts
1675
1798
  var API_VERSION7 = "^0.1.10";
1676
1799
  function formatNextRun(intervalMs) {
1677
- const ms = isNaN(intervalMs) || intervalMs <= 0 ? 6e4 : intervalMs;
1800
+ const ms = Number.isNaN(intervalMs) || intervalMs <= 0 ? 6e4 : intervalMs;
1678
1801
  return new Date(Date.now() + ms).toISOString();
1679
1802
  }
1680
1803
  var plugin8 = {
@@ -1797,7 +1920,7 @@ var plugin8 = {
1797
1920
  if (!name || typeof name !== "string" || name.trim() === "") {
1798
1921
  return { ok: false, error: "name is required and must be a non-empty string" };
1799
1922
  }
1800
- if (isNaN(intervalMs)) {
1923
+ if (Number.isNaN(intervalMs)) {
1801
1924
  return { ok: false, error: "intervalMs must be a number >= 1000" };
1802
1925
  }
1803
1926
  if (state.jobs.has(name)) {
@@ -1892,8 +2015,6 @@ var plugin8 = {
1892
2015
  }
1893
2016
  };
1894
2017
  var cron_default = plugin8;
1895
-
1896
- // src/template-engine/index.ts
1897
2018
  var API_VERSION8 = "^0.1.10";
1898
2019
  function expandTemplate(template, variables) {
1899
2020
  let result = template;
@@ -2002,6 +2123,9 @@ var plugin9 = {
2002
2123
  return { ok: false, error: String(err) };
2003
2124
  }
2004
2125
  if (outputPath) {
2126
+ if (isAbsolute(outputPath) || outputPath.includes("..")) {
2127
+ return { ok: false, error: 'outputPath must be a relative path without ".." components' };
2128
+ }
2005
2129
  const { writeFileSync } = await import('fs');
2006
2130
  writeFileSync(outputPath, result, "utf-8");
2007
2131
  return {
@@ -2063,6 +2187,9 @@ var plugin9 = {
2063
2187
  return { ok: false, error: `Template rendering failed: ${err}` };
2064
2188
  }
2065
2189
  if (outputPath) {
2190
+ if (isAbsolute(outputPath) || outputPath.includes("..")) {
2191
+ return { ok: false, error: 'outputPath must be a relative path without ".." components' };
2192
+ }
2066
2193
  const { writeFileSync } = await import('fs');
2067
2194
  writeFileSync(outputPath, result, "utf-8");
2068
2195
  return {
@@ -2161,7 +2288,7 @@ var template_engine_default = plugin9;
2161
2288
  var API_VERSION9 = "^0.1.10";
2162
2289
  function runGit2(args, cwd) {
2163
2290
  try {
2164
- return execSync(`git ${args.join(" ")}`, {
2291
+ return execFileSync("git", args, {
2165
2292
  encoding: "utf-8",
2166
2293
  cwd,
2167
2294
  stdio: ["pipe", "pipe", "pipe"],
@@ -2174,10 +2301,10 @@ function runGit2(args, cwd) {
2174
2301
  }
2175
2302
  }
2176
2303
  function getPackageJson(cwd) {
2177
- const path = cwd ? `${cwd}/package.json` : "package.json";
2178
- if (!existsSync(path)) return null;
2304
+ const path2 = cwd ? `${cwd}/package.json` : "package.json";
2305
+ if (!existsSync(path2)) return null;
2179
2306
  try {
2180
- return JSON.parse(readFileSync(path, "utf-8"));
2307
+ return JSON.parse(readFileSync(path2, "utf-8"));
2181
2308
  } catch {
2182
2309
  return null;
2183
2310
  }
@@ -2185,7 +2312,7 @@ function getPackageJson(cwd) {
2185
2312
  function parseVersion(v) {
2186
2313
  const m = v.match(/^v?(\d+)\.(\d+)\.(\d+)/);
2187
2314
  if (!m) return [0, 0, 0];
2188
- return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])];
2315
+ return [Number.parseInt(m[1]), Number.parseInt(m[2]), Number.parseInt(m[3])];
2189
2316
  }
2190
2317
  function bumpVersion(version, part) {
2191
2318
  let [major, minor, patch] = parseVersion(version);
@@ -2421,7 +2548,7 @@ var plugin10 = {
2421
2548
  latestTag = tagsOutput || null;
2422
2549
  if (latestTag) {
2423
2550
  const countOutput = runGit2(["rev-list", "--count", `${latestTag}..HEAD`], cwd);
2424
- commitsSinceTag = parseInt(countOutput) || 0;
2551
+ commitsSinceTag = Number.parseInt(countOutput) || 0;
2425
2552
  }
2426
2553
  } catch {
2427
2554
  latestTag = null;
package/dist/json-path.js CHANGED
@@ -13,7 +13,7 @@ function jmespathSearch(data, query) {
13
13
  }
14
14
  const arrMatch = query.match(/^\[(\d+)\](?:\.(.+))?$/);
15
15
  if (arrMatch) {
16
- const idx = parseInt(arrMatch[1], 10);
16
+ const idx = Number.parseInt(arrMatch[1], 10);
17
17
  const rest = arrMatch[2];
18
18
  const arr = data;
19
19
  const val = arr?.[idx];
@@ -48,9 +48,9 @@ function jmespathSearch(data, query) {
48
48
  const itemVal = item[field];
49
49
  switch (op) {
50
50
  case "==":
51
- return itemVal == cmpVal;
51
+ return itemVal === cmpVal;
52
52
  case "!=":
53
- return itemVal != cmpVal;
53
+ return itemVal !== cmpVal;
54
54
  case ">":
55
55
  return Number(itemVal) > Number(cmpVal);
56
56
  case "<":
@@ -1,11 +1,11 @@
1
- import { execSync } from 'child_process';
1
+ import { execFileSync } from 'child_process';
2
2
  import { existsSync, readFileSync } from 'fs';
3
3
 
4
4
  // src/semver-bump/index.ts
5
5
  var API_VERSION = "^0.1.10";
6
6
  function runGit(args, cwd) {
7
7
  try {
8
- return execSync(`git ${args.join(" ")}`, {
8
+ return execFileSync("git", args, {
9
9
  encoding: "utf-8",
10
10
  cwd,
11
11
  stdio: ["pipe", "pipe", "pipe"],
@@ -29,7 +29,7 @@ function getPackageJson(cwd) {
29
29
  function parseVersion(v) {
30
30
  const m = v.match(/^v?(\d+)\.(\d+)\.(\d+)/);
31
31
  if (!m) return [0, 0, 0];
32
- return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])];
32
+ return [Number.parseInt(m[1]), Number.parseInt(m[2]), Number.parseInt(m[3])];
33
33
  }
34
34
  function bumpVersion(version, part) {
35
35
  let [major, minor, patch] = parseVersion(version);
@@ -265,7 +265,7 @@ var plugin = {
265
265
  latestTag = tagsOutput || null;
266
266
  if (latestTag) {
267
267
  const countOutput = runGit(["rev-list", "--count", `${latestTag}..HEAD`], cwd);
268
- commitsSinceTag = parseInt(countOutput) || 0;
268
+ commitsSinceTag = Number.parseInt(countOutput) || 0;
269
269
  }
270
270
  } catch {
271
271
  latestTag = null;
@@ -1,4 +1,4 @@
1
- import { execSync } from 'child_process';
1
+ import { execSync, execFileSync } from 'child_process';
2
2
  import { existsSync, readdirSync } from 'fs';
3
3
  import { join } from 'path';
4
4
 
@@ -27,7 +27,7 @@ function runShellCheck(files, severity, cwd) {
27
27
  ];
28
28
  let raw;
29
29
  try {
30
- raw = execSync(`shellcheck ${args.join(" ")}`, {
30
+ raw = execFileSync("shellcheck", args, {
31
31
  encoding: "utf-8",
32
32
  cwd,
33
33
  stdio: ["pipe", "pipe", "pipe"],
@@ -122,7 +122,7 @@ var plugin = {
122
122
  required: ["files"]
123
123
  },
124
124
  permission: "auto",
125
- mutating: false,
125
+ mutating: true,
126
126
  async execute(input) {
127
127
  const files = input["files"];
128
128
  const severity = input["severity"] ?? "warning";
@@ -135,7 +135,10 @@ var plugin = {
135
135
  }
136
136
  const byFile = {};
137
137
  for (const issue of issues) {
138
- (byFile[issue.file] ??= []).push(issue);
138
+ if (byFile[issue.file] === void 0) {
139
+ byFile[issue.file] = [];
140
+ }
141
+ byFile[issue.file].push(issue);
139
142
  }
140
143
  const errorCount = issues.filter((i) => i.level === "error").length;
141
144
  const warningCount = issues.filter((i) => i.level === "warning").length;
@@ -183,7 +186,7 @@ var plugin = {
183
186
  }
184
187
  },
185
188
  permission: "auto",
186
- mutating: false,
189
+ mutating: true,
187
190
  async execute(input) {
188
191
  const dir = input["directory"] ?? ".";
189
192
  const pattern = input["pattern"] ?? "";
@@ -201,7 +204,10 @@ var plugin = {
201
204
  }
202
205
  const byFile = {};
203
206
  for (const issue of issues) {
204
- (byFile[issue.file] ??= []).push(issue);
207
+ if (byFile[issue.file] === void 0) {
208
+ byFile[issue.file] = [];
209
+ }
210
+ byFile[issue.file].push(issue);
205
211
  }
206
212
  return {
207
213
  ok: true,
@@ -1,3 +1,5 @@
1
+ import { isAbsolute } from 'path';
2
+
1
3
  // src/template-engine/index.ts
2
4
  var API_VERSION = "^0.1.10";
3
5
  function expandTemplate(template, variables) {
@@ -107,6 +109,9 @@ var plugin = {
107
109
  return { ok: false, error: String(err) };
108
110
  }
109
111
  if (outputPath) {
112
+ if (isAbsolute(outputPath) || outputPath.includes("..")) {
113
+ return { ok: false, error: 'outputPath must be a relative path without ".." components' };
114
+ }
110
115
  const { writeFileSync } = await import('fs');
111
116
  writeFileSync(outputPath, result, "utf-8");
112
117
  return {
@@ -168,6 +173,9 @@ var plugin = {
168
173
  return { ok: false, error: `Template rendering failed: ${err}` };
169
174
  }
170
175
  if (outputPath) {
176
+ if (isAbsolute(outputPath) || outputPath.includes("..")) {
177
+ return { ok: false, error: 'outputPath must be a relative path without ".." components' };
178
+ }
171
179
  const { writeFileSync } = await import('fs');
172
180
  writeFileSync(outputPath, result, "utf-8");
173
181
  return {
@@ -1,13 +1,5 @@
1
1
  import { Plugin } from '@wrongstack/core';
2
2
 
3
- /**
4
- * web-search plugin — Cached web search with deduplication and ranking.
5
- *
6
- * Tools registered:
7
- * - web_search: Search the web with caching and deduplication
8
- * - web_fetch: Fetch a URL and return content as markdown
9
- */
10
-
11
3
  declare const plugin: Plugin;
12
4
 
13
5
  export { plugin as default };
@@ -1,3 +1,6 @@
1
+ import { lookup } from 'dns/promises';
2
+ import { isIPv4, isIPv6 } from 'net';
3
+
1
4
  // src/web-search/index.ts
2
5
  var API_VERSION = "^0.1.10";
3
6
  async function duckduckgoSearch(query, numResults) {
@@ -28,13 +31,81 @@ async function duckduckgoSearch(query, numResults) {
28
31
  }
29
32
  return results;
30
33
  }
34
+ function isPrivateIPv4(host) {
35
+ const parts = host.split(".").map(Number);
36
+ if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return true;
37
+ const [a, b] = parts;
38
+ return a === 0 || // 0.0.0.0/8 "this host"
39
+ a === 10 || // private
40
+ a === 127 || // loopback
41
+ a === 100 && b >= 64 && b <= 127 || // CGNAT 100.64/10
42
+ a === 169 && b === 254 || // link-local incl. 169.254.169.254 (cloud IMDS)
43
+ a === 172 && b >= 16 && b <= 31 || // private
44
+ a === 192 && b === 168 || // private
45
+ a >= 224;
46
+ }
47
+ function isPrivateIPv6(raw) {
48
+ const host = raw.toLowerCase();
49
+ if (host === "::1" || host === "::") return true;
50
+ const mapped = host.match(/(?:::ffff:)?(\d+\.\d+\.\d+\.\d+)$/);
51
+ if (mapped?.[1]) return isPrivateIPv4(mapped[1]);
52
+ if (host.startsWith("fc") || host.startsWith("fd")) return true;
53
+ if (host.startsWith("fe8") || host.startsWith("fe9") || host.startsWith("fea") || host.startsWith("feb"))
54
+ return true;
55
+ return false;
56
+ }
57
+ function assertSafeIp(ip) {
58
+ if (isIPv4(ip) && isPrivateIPv4(ip)) {
59
+ throw new Error(`Blocked private/loopback address: ${ip}`);
60
+ }
61
+ if (isIPv6(ip) && isPrivateIPv6(ip)) {
62
+ throw new Error(`Blocked private/loopback address: ${ip}`);
63
+ }
64
+ }
65
+ async function assertSafeUrl(rawUrl) {
66
+ const u = new URL(rawUrl);
67
+ if (u.protocol !== "http:" && u.protocol !== "https:") {
68
+ throw new Error(`Unsupported protocol: ${u.protocol}`);
69
+ }
70
+ const host = u.hostname.startsWith("[") && u.hostname.endsWith("]") ? u.hostname.slice(1, -1) : u.hostname;
71
+ if (host === "localhost" || host.endsWith(".localhost") || host === "" || host === "0.0.0.0") {
72
+ throw new Error("Blocked localhost target");
73
+ }
74
+ if (isIPv4(host) || isIPv6(host)) {
75
+ assertSafeIp(host);
76
+ return;
77
+ }
78
+ let addrs;
79
+ try {
80
+ addrs = await lookup(host, { all: true });
81
+ } catch {
82
+ throw new Error(`Could not resolve host: ${host}`);
83
+ }
84
+ for (const { address } of addrs) assertSafeIp(address);
85
+ }
31
86
  async function fetchUrl(url, format) {
32
- const resp = await fetch(url, {
33
- headers: {
34
- "User-Agent": "Mozilla/5.0 (compatible; WrongStack/1.0; +https://wrongstack.com)",
35
- "Accept": format === "text" ? "text/plain" : "text/html"
87
+ const MAX_REDIRECTS = 5;
88
+ let currentUrl = url;
89
+ let resp;
90
+ for (let i = 0; i <= MAX_REDIRECTS; i++) {
91
+ await assertSafeUrl(currentUrl);
92
+ resp = await fetch(currentUrl, {
93
+ redirect: "manual",
94
+ headers: {
95
+ "User-Agent": "Mozilla/5.0 (compatible; WrongStack/1.0; +https://wrongstack.com)",
96
+ Accept: format === "text" ? "text/plain" : "text/html"
97
+ }
98
+ });
99
+ if (resp.status >= 300 && resp.status < 400) {
100
+ const loc = resp.headers.get("location");
101
+ if (!loc) break;
102
+ currentUrl = new URL(loc, currentUrl).toString();
103
+ if (i === MAX_REDIRECTS) throw new Error("Too many redirects");
104
+ continue;
36
105
  }
37
- });
106
+ break;
107
+ }
108
+ if (!resp) throw new Error(`Failed to fetch ${url}`);
38
109
  if (!resp.ok) throw new Error(`Failed to fetch ${url}: ${resp.status} ${resp.statusText}`);
39
110
  if (format === "text") {
40
111
  return resp.text();
@@ -108,7 +179,7 @@ var plugin = {
108
179
  required: ["query"]
109
180
  },
110
181
  permission: "auto",
111
- mutating: false,
182
+ mutating: true,
112
183
  async execute(input) {
113
184
  const query = input["query"];
114
185
  if (!query || typeof query !== "string" || query.trim() === "") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/plugins",
3
- "version": "0.1.0",
3
+ "version": "0.9.1",
4
4
  "description": "Official WrongStack plugin collection — auto-doc, git-autocommit, shell-check, cost-tracker, file-watcher, web-search, json-path, cron, template-engine, semver-bump",
5
5
  "license": "MIT",
6
6
  "author": "ECOSTACK TECHNOLOGY OÜ",
@@ -63,7 +63,7 @@
63
63
  "vitest": "^3.0.0"
64
64
  },
65
65
  "dependencies": {
66
- "@wrongstack/core": "0.6.4"
66
+ "@wrongstack/core": "0.9.1"
67
67
  },
68
68
  "scripts": {
69
69
  "build": "tsup",