@wrongstack/plugins 0.309.0 → 0.310.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.
@@ -45,6 +45,8 @@ export interface ModelPricing {
45
45
  input: number;
46
46
  /** Cost per 1M output (completion) tokens in USD. */
47
47
  output: number;
48
+ /** Cost per 1M prompt-cache read tokens; input rate is the safe fallback. */
49
+ cacheRead?: number | undefined;
48
50
  }
49
51
  declare const plugin: Plugin;
50
52
  export default plugin;
@@ -12,13 +12,15 @@ var __copyProps = (to, from, except, desc) => {
12
12
  };
13
13
  var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
14
14
 
15
+ // src/cost-tracker/index.ts
16
+ import { expectDefined } from "@wrongstack/core/utils";
17
+
15
18
  // src/runtime/index.ts
16
19
  var runtime_exports = {};
17
20
  __reExport(runtime_exports, runtime_star);
18
21
  import * as runtime_star from "@wrongstack/plugin-sdk/runtime";
19
22
 
20
23
  // src/cost-tracker/index.ts
21
- import { expectDefined } from "@wrongstack/core/utils";
22
24
  var API_VERSION = "^0.1.10";
23
25
  var PRICING = {
24
26
  "gpt-4o": { input: 5, output: 15 },
@@ -48,14 +50,14 @@ function readCostTrackerConfig(raw) {
48
50
  };
49
51
  }
50
52
  var modelKeyCache = new runtime_exports.BoundedMap({ max: 256 });
51
- function estimateCost(model, promptTokens, completionTokens) {
53
+ function estimateCost(model, freshTokens, completionTokens, cachedTokens = 0) {
52
54
  let key = modelKeyCache.get(model);
53
55
  if (!key) {
54
56
  key = model.toLowerCase();
55
57
  modelKeyCache.set(model, key);
56
58
  }
57
59
  const pricing = pricingOverrides[key] ?? bundledFromRegistry[key] ?? PRICING[key] ?? DEFAULT_PRICING;
58
- const inputCost = promptTokens / 1e6 * pricing.input;
60
+ const inputCost = freshTokens / 1e6 * pricing.input + cachedTokens / 1e6 * (pricing.cacheRead ?? pricing.input);
59
61
  const outputCost = completionTokens / 1e6 * pricing.output;
60
62
  return inputCost + outputCost;
61
63
  }
@@ -89,12 +91,17 @@ var plugin = {
89
91
  },
90
92
  pricingOverrides: {
91
93
  type: "object",
92
- description: "Per-model pricing overrides in USD per 1M tokens. Keys are lowercased model names; values are { input, output }. Takes precedence over the bundled PRICING table.",
94
+ description: "Per-model pricing overrides in USD per 1M tokens. Values are { input, output, cacheRead? }.",
93
95
  additionalProperties: {
94
96
  type: "object",
95
97
  properties: {
96
98
  input: { type: "number", minimum: 0, description: "Cost per 1M input tokens in USD" },
97
- output: { type: "number", minimum: 0, description: "Cost per 1M output tokens in USD" }
99
+ output: { type: "number", minimum: 0, description: "Cost per 1M output tokens in USD" },
100
+ cacheRead: {
101
+ type: "number",
102
+ minimum: 0,
103
+ description: "Cost per 1M prompt-cache read tokens in USD"
104
+ }
98
105
  },
99
106
  required: ["input", "output"],
100
107
  additionalProperties: false
@@ -127,7 +134,12 @@ var plugin = {
127
134
  const input = v["input"];
128
135
  const output = v["output"];
129
136
  if (typeof input !== "number" || typeof output !== "number") continue;
130
- pricingOverrides[model.toLowerCase()] = { input, output };
137
+ const cacheRead = v["cacheRead"];
138
+ pricingOverrides[model.toLowerCase()] = {
139
+ input,
140
+ output,
141
+ ...typeof cacheRead === "number" ? { cacheRead } : {}
142
+ };
131
143
  }
132
144
  }
133
145
  if (api.modelsRegistry) {
@@ -142,7 +154,8 @@ var plugin = {
142
154
  if (cost && typeof cost.input === "number" && typeof cost.output === "number") {
143
155
  bundledFromRegistry[modelId.toLowerCase()] = {
144
156
  input: cost.input,
145
- output: cost.output
157
+ output: cost.output,
158
+ ...typeof cost.cache_read === "number" ? { cacheRead: cost.cache_read } : {}
146
159
  };
147
160
  hydrated += 1;
148
161
  }
@@ -169,10 +182,12 @@ var plugin = {
169
182
  api.onEvent("provider.response", async (payload) => {
170
183
  const usage = payload.usage;
171
184
  const model = payload.ctx?.model ?? "unknown";
172
- const promptTokens = usage.input ?? 0;
185
+ const cachedTokens = usage.cacheRead ?? 0;
186
+ const freshTokens = (usage.input ?? 0) + (usage.cacheWrite ?? 0);
187
+ const promptTokens = freshTokens + cachedTokens;
173
188
  const completionTokens = usage.output ?? 0;
174
189
  const totalTokens = promptTokens + completionTokens;
175
- const costUsd = estimateCost(model, promptTokens, completionTokens);
190
+ const costUsd = estimateCost(model, freshTokens, completionTokens, cachedTokens);
176
191
  const record = {
177
192
  promptTokens,
178
193
  completionTokens,
@@ -402,6 +402,7 @@ var state2 = {
402
402
  auditsRun: 0,
403
403
  blocks: 0,
404
404
  warns: 0,
405
+ belowThresholdWarns: 0,
405
406
  errors: 0,
406
407
  lastResult: null,
407
408
  hookUnregister: null
@@ -449,9 +450,11 @@ function parseAuditJson(jsonString) {
449
450
  return null;
450
451
  }
451
452
  const out = { maxSeverity: null, counts: {}, total: 0 };
452
- if (parsed && typeof parsed === "object") {
453
+ let recognizedShape = false;
454
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
453
455
  const vulnerabilities = parsed["vulnerabilities"];
454
456
  if (vulnerabilities && typeof vulnerabilities === "object" && !Array.isArray(vulnerabilities)) {
457
+ recognizedShape = true;
455
458
  for (const entry of Object.values(vulnerabilities)) {
456
459
  if (entry && typeof entry === "object") {
457
460
  const severity = entry["severity"];
@@ -464,7 +467,8 @@ function parseAuditJson(jsonString) {
464
467
  if (out.total === 0) {
465
468
  const metadata = parsed["metadata"];
466
469
  const metaVulns = metadata && typeof metadata === "object" ? metadata["vulnerabilities"] : void 0;
467
- if (metaVulns && typeof metaVulns === "object") {
470
+ if (metaVulns && typeof metaVulns === "object" && !Array.isArray(metaVulns)) {
471
+ recognizedShape = true;
468
472
  for (const [key, value] of Object.entries(metaVulns)) {
469
473
  const count = typeof value === "number" ? value : 0;
470
474
  if (count > 0 && SEVERITY_RANK[key] != null) {
@@ -480,6 +484,7 @@ function parseAuditJson(jsonString) {
480
484
  }
481
485
  }
482
486
  if (Array.isArray(parsed)) {
487
+ recognizedShape = true;
483
488
  for (const entry of parsed) {
484
489
  if (entry && typeof entry === "object") {
485
490
  const severity = entry["severity"];
@@ -489,6 +494,7 @@ function parseAuditJson(jsonString) {
489
494
  }
490
495
  }
491
496
  }
497
+ if (!recognizedShape) return null;
492
498
  return out;
493
499
  }
494
500
  function exceedsThreshold(report, threshold) {
@@ -598,6 +604,7 @@ var plugin2 = {
598
604
  state2.auditsRun = 0;
599
605
  state2.blocks = 0;
600
606
  state2.warns = 0;
607
+ state2.belowThresholdWarns = 0;
601
608
  state2.errors = 0;
602
609
  state2.lastResult = null;
603
610
  if (state2.hookUnregister) {
@@ -618,7 +625,13 @@ var plugin2 = {
618
625
  const result = await runAudit(cfg);
619
626
  if (!result) {
620
627
  state2.errors += 1;
621
- return;
628
+ api.metrics.counter("audit_errors");
629
+ api.log.warn("dependency-vulnerability-gate: audit did not complete; install not vetted", {
630
+ managerHint: "see dependency_audit_status for counters"
631
+ });
632
+ return {
633
+ additionalContext: "\n\u26A0\uFE0F dependency-vulnerability-gate: the package audit could not be completed (timeout or unreadable output), so these dependencies were NOT checked for known vulnerabilities."
634
+ };
622
635
  }
623
636
  state2.auditsRun += 1;
624
637
  state2.lastResult = {
@@ -630,6 +643,17 @@ var plugin2 = {
630
643
  when: (/* @__PURE__ */ new Date()).toISOString()
631
644
  };
632
645
  if (!exceedsThreshold(result.report, cfg.severityThreshold)) {
646
+ if (result.report.total > 0) {
647
+ state2.belowThresholdWarns += 1;
648
+ api.metrics.counter("below_threshold_warns");
649
+ const belowCounts = Object.entries(result.report.counts).filter(([, count]) => count > 0).map(([sev, count]) => `${sev}: ${count}`).join(", ");
650
+ return {
651
+ additionalContext: `
652
+ \u2139\uFE0F dependency-vulnerability-gate: ${result.manager} audit found vulnerabilities BELOW the configured threshold (${cfg.severityThreshold}); not blocking.
653
+ Max severity: ${result.report.maxSeverity}
654
+ Counts: ${belowCounts || "unknown"}`
655
+ };
656
+ }
633
657
  return;
634
658
  }
635
659
  const countsText = Object.entries(result.report.counts).filter(([, count]) => count > 0).map(([sev, count]) => `${sev}: ${count}`).join(", ");
@@ -671,6 +695,7 @@ Review the audit output or adjust the dependency choice.`;
671
695
  auditsRun: state2.auditsRun,
672
696
  blocks: state2.blocks,
673
697
  warns: state2.warns,
698
+ belowThresholdWarns: state2.belowThresholdWarns,
674
699
  errors: state2.errors
675
700
  },
676
701
  lastResult: state2.lastResult
@@ -697,6 +722,7 @@ Review the audit output or adjust the dependency choice.`;
697
722
  auditsRun: state2.auditsRun,
698
723
  blocks: state2.blocks,
699
724
  warns: state2.warns,
725
+ belowThresholdWarns: state2.belowThresholdWarns,
700
726
  errors: state2.errors
701
727
  };
702
728
  state2.invocations = 0;
@@ -704,6 +730,7 @@ Review the audit output or adjust the dependency choice.`;
704
730
  state2.auditsRun = 0;
705
731
  state2.blocks = 0;
706
732
  state2.warns = 0;
733
+ state2.belowThresholdWarns = 0;
707
734
  state2.errors = 0;
708
735
  state2.lastResult = null;
709
736
  api.log.info("dependency-vulnerability-gate: teardown complete", { final });
@@ -718,6 +745,7 @@ Review the audit output or adjust the dependency choice.`;
718
745
  auditsRun: state2.auditsRun,
719
746
  blocks: state2.blocks,
720
747
  warns: state2.warns,
748
+ belowThresholdWarns: state2.belowThresholdWarns,
721
749
  errors: state2.errors
722
750
  },
723
751
  lastResult: state2.lastResult
package/dist/index.js CHANGED
@@ -4370,14 +4370,14 @@ function readCostTrackerConfig(raw) {
4370
4370
  };
4371
4371
  }
4372
4372
  var modelKeyCache = new runtime_exports.BoundedMap({ max: 256 });
4373
- function estimateCost(model, promptTokens, completionTokens) {
4373
+ function estimateCost(model, freshTokens, completionTokens, cachedTokens = 0) {
4374
4374
  let key = modelKeyCache.get(model);
4375
4375
  if (!key) {
4376
4376
  key = model.toLowerCase();
4377
4377
  modelKeyCache.set(model, key);
4378
4378
  }
4379
4379
  const pricing = pricingOverrides[key] ?? bundledFromRegistry[key] ?? PRICING[key] ?? DEFAULT_PRICING;
4380
- const inputCost = promptTokens / 1e6 * pricing.input;
4380
+ const inputCost = freshTokens / 1e6 * pricing.input + cachedTokens / 1e6 * (pricing.cacheRead ?? pricing.input);
4381
4381
  const outputCost = completionTokens / 1e6 * pricing.output;
4382
4382
  return inputCost + outputCost;
4383
4383
  }
@@ -4411,12 +4411,17 @@ var plugin14 = {
4411
4411
  },
4412
4412
  pricingOverrides: {
4413
4413
  type: "object",
4414
- description: "Per-model pricing overrides in USD per 1M tokens. Keys are lowercased model names; values are { input, output }. Takes precedence over the bundled PRICING table.",
4414
+ description: "Per-model pricing overrides in USD per 1M tokens. Values are { input, output, cacheRead? }.",
4415
4415
  additionalProperties: {
4416
4416
  type: "object",
4417
4417
  properties: {
4418
4418
  input: { type: "number", minimum: 0, description: "Cost per 1M input tokens in USD" },
4419
- output: { type: "number", minimum: 0, description: "Cost per 1M output tokens in USD" }
4419
+ output: { type: "number", minimum: 0, description: "Cost per 1M output tokens in USD" },
4420
+ cacheRead: {
4421
+ type: "number",
4422
+ minimum: 0,
4423
+ description: "Cost per 1M prompt-cache read tokens in USD"
4424
+ }
4420
4425
  },
4421
4426
  required: ["input", "output"],
4422
4427
  additionalProperties: false
@@ -4449,7 +4454,12 @@ var plugin14 = {
4449
4454
  const input = v["input"];
4450
4455
  const output = v["output"];
4451
4456
  if (typeof input !== "number" || typeof output !== "number") continue;
4452
- pricingOverrides[model.toLowerCase()] = { input, output };
4457
+ const cacheRead = v["cacheRead"];
4458
+ pricingOverrides[model.toLowerCase()] = {
4459
+ input,
4460
+ output,
4461
+ ...typeof cacheRead === "number" ? { cacheRead } : {}
4462
+ };
4453
4463
  }
4454
4464
  }
4455
4465
  if (api.modelsRegistry) {
@@ -4464,7 +4474,8 @@ var plugin14 = {
4464
4474
  if (cost && typeof cost.input === "number" && typeof cost.output === "number") {
4465
4475
  bundledFromRegistry[modelId.toLowerCase()] = {
4466
4476
  input: cost.input,
4467
- output: cost.output
4477
+ output: cost.output,
4478
+ ...typeof cost.cache_read === "number" ? { cacheRead: cost.cache_read } : {}
4468
4479
  };
4469
4480
  hydrated += 1;
4470
4481
  }
@@ -4491,10 +4502,12 @@ var plugin14 = {
4491
4502
  api.onEvent("provider.response", async (payload) => {
4492
4503
  const usage = payload.usage;
4493
4504
  const model = payload.ctx?.model ?? "unknown";
4494
- const promptTokens = usage.input ?? 0;
4505
+ const cachedTokens = usage.cacheRead ?? 0;
4506
+ const freshTokens = (usage.input ?? 0) + (usage.cacheWrite ?? 0);
4507
+ const promptTokens = freshTokens + cachedTokens;
4495
4508
  const completionTokens = usage.output ?? 0;
4496
4509
  const totalTokens = promptTokens + completionTokens;
4497
- const costUsd = estimateCost(model, promptTokens, completionTokens);
4510
+ const costUsd = estimateCost(model, freshTokens, completionTokens, cachedTokens);
4498
4511
  const record = {
4499
4512
  promptTokens,
4500
4513
  completionTokens,
@@ -5713,6 +5726,7 @@ var state17 = {
5713
5726
  auditsRun: 0,
5714
5727
  blocks: 0,
5715
5728
  warns: 0,
5729
+ belowThresholdWarns: 0,
5716
5730
  errors: 0,
5717
5731
  lastResult: null,
5718
5732
  hookUnregister: null
@@ -5760,9 +5774,11 @@ function parseAuditJson(jsonString) {
5760
5774
  return null;
5761
5775
  }
5762
5776
  const out = { maxSeverity: null, counts: {}, total: 0 };
5763
- if (parsed && typeof parsed === "object") {
5777
+ let recognizedShape = false;
5778
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
5764
5779
  const vulnerabilities = parsed["vulnerabilities"];
5765
5780
  if (vulnerabilities && typeof vulnerabilities === "object" && !Array.isArray(vulnerabilities)) {
5781
+ recognizedShape = true;
5766
5782
  for (const entry of Object.values(vulnerabilities)) {
5767
5783
  if (entry && typeof entry === "object") {
5768
5784
  const severity = entry["severity"];
@@ -5775,7 +5791,8 @@ function parseAuditJson(jsonString) {
5775
5791
  if (out.total === 0) {
5776
5792
  const metadata = parsed["metadata"];
5777
5793
  const metaVulns = metadata && typeof metadata === "object" ? metadata["vulnerabilities"] : void 0;
5778
- if (metaVulns && typeof metaVulns === "object") {
5794
+ if (metaVulns && typeof metaVulns === "object" && !Array.isArray(metaVulns)) {
5795
+ recognizedShape = true;
5779
5796
  for (const [key, value] of Object.entries(metaVulns)) {
5780
5797
  const count = typeof value === "number" ? value : 0;
5781
5798
  if (count > 0 && SEVERITY_RANK[key] != null) {
@@ -5791,6 +5808,7 @@ function parseAuditJson(jsonString) {
5791
5808
  }
5792
5809
  }
5793
5810
  if (Array.isArray(parsed)) {
5811
+ recognizedShape = true;
5794
5812
  for (const entry of parsed) {
5795
5813
  if (entry && typeof entry === "object") {
5796
5814
  const severity = entry["severity"];
@@ -5800,6 +5818,7 @@ function parseAuditJson(jsonString) {
5800
5818
  }
5801
5819
  }
5802
5820
  }
5821
+ if (!recognizedShape) return null;
5803
5822
  return out;
5804
5823
  }
5805
5824
  function exceedsThreshold(report, threshold) {
@@ -5909,6 +5928,7 @@ var plugin18 = {
5909
5928
  state17.auditsRun = 0;
5910
5929
  state17.blocks = 0;
5911
5930
  state17.warns = 0;
5931
+ state17.belowThresholdWarns = 0;
5912
5932
  state17.errors = 0;
5913
5933
  state17.lastResult = null;
5914
5934
  if (state17.hookUnregister) {
@@ -5929,7 +5949,13 @@ var plugin18 = {
5929
5949
  const result = await runAudit(cfg);
5930
5950
  if (!result) {
5931
5951
  state17.errors += 1;
5932
- return;
5952
+ api.metrics.counter("audit_errors");
5953
+ api.log.warn("dependency-vulnerability-gate: audit did not complete; install not vetted", {
5954
+ managerHint: "see dependency_audit_status for counters"
5955
+ });
5956
+ return {
5957
+ additionalContext: "\n\u26A0\uFE0F dependency-vulnerability-gate: the package audit could not be completed (timeout or unreadable output), so these dependencies were NOT checked for known vulnerabilities."
5958
+ };
5933
5959
  }
5934
5960
  state17.auditsRun += 1;
5935
5961
  state17.lastResult = {
@@ -5941,6 +5967,17 @@ var plugin18 = {
5941
5967
  when: (/* @__PURE__ */ new Date()).toISOString()
5942
5968
  };
5943
5969
  if (!exceedsThreshold(result.report, cfg.severityThreshold)) {
5970
+ if (result.report.total > 0) {
5971
+ state17.belowThresholdWarns += 1;
5972
+ api.metrics.counter("below_threshold_warns");
5973
+ const belowCounts = Object.entries(result.report.counts).filter(([, count]) => count > 0).map(([sev, count]) => `${sev}: ${count}`).join(", ");
5974
+ return {
5975
+ additionalContext: `
5976
+ \u2139\uFE0F dependency-vulnerability-gate: ${result.manager} audit found vulnerabilities BELOW the configured threshold (${cfg.severityThreshold}); not blocking.
5977
+ Max severity: ${result.report.maxSeverity}
5978
+ Counts: ${belowCounts || "unknown"}`
5979
+ };
5980
+ }
5944
5981
  return;
5945
5982
  }
5946
5983
  const countsText = Object.entries(result.report.counts).filter(([, count]) => count > 0).map(([sev, count]) => `${sev}: ${count}`).join(", ");
@@ -5982,6 +6019,7 @@ Review the audit output or adjust the dependency choice.`;
5982
6019
  auditsRun: state17.auditsRun,
5983
6020
  blocks: state17.blocks,
5984
6021
  warns: state17.warns,
6022
+ belowThresholdWarns: state17.belowThresholdWarns,
5985
6023
  errors: state17.errors
5986
6024
  },
5987
6025
  lastResult: state17.lastResult
@@ -6008,6 +6046,7 @@ Review the audit output or adjust the dependency choice.`;
6008
6046
  auditsRun: state17.auditsRun,
6009
6047
  blocks: state17.blocks,
6010
6048
  warns: state17.warns,
6049
+ belowThresholdWarns: state17.belowThresholdWarns,
6011
6050
  errors: state17.errors
6012
6051
  };
6013
6052
  state17.invocations = 0;
@@ -6015,6 +6054,7 @@ Review the audit output or adjust the dependency choice.`;
6015
6054
  state17.auditsRun = 0;
6016
6055
  state17.blocks = 0;
6017
6056
  state17.warns = 0;
6057
+ state17.belowThresholdWarns = 0;
6018
6058
  state17.errors = 0;
6019
6059
  state17.lastResult = null;
6020
6060
  api.log.info("dependency-vulnerability-gate: teardown complete", { final });
@@ -6029,6 +6069,7 @@ Review the audit output or adjust the dependency choice.`;
6029
6069
  auditsRun: state17.auditsRun,
6030
6070
  blocks: state17.blocks,
6031
6071
  warns: state17.warns,
6072
+ belowThresholdWarns: state17.belowThresholdWarns,
6032
6073
  errors: state17.errors
6033
6074
  },
6034
6075
  lastResult: state17.lastResult
@@ -13612,7 +13653,7 @@ function stripLauncherAtBoundary(command, launcher, valueTaking) {
13612
13653
  }
13613
13654
  return `${command.slice(0, match.index)}${boundary}${after.slice(consumed).replace(/^\s+/, "")}`;
13614
13655
  }
13615
- var XARGS_OPTIONS = String.raw`(?:\s+(?:(?:-[InLsPjeE]|--(?:arg-file|replace|max-args|max-lines|max-chars|max-procs))\s+[^\s]+|-[^\s]+))*`;
13656
+ var XARGS_OPTIONS = String.raw`(?:\s+(?:(?:-[InLsPjeE]|--(?:arg-file|replace|max-args|max-lines|max-chars|max-procs))\s+[^\s-][^\s]*|-[^\s]+))*`;
13616
13657
  var COMMAND_BOUNDARY = String.raw`(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs${XARGS_OPTIONS}\s+)`;
13617
13658
  function commandRecursivelyDeletes(command) {
13618
13659
  const stripped = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
@@ -13944,19 +13985,13 @@ function destructiveTargetsAtDepth(command, depth) {
13944
13985
  if (destination) targets.push(destination);
13945
13986
  c = copy.exec(normalizedCommand);
13946
13987
  }
13947
- const tee = new RegExp(
13948
- String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(tee)\s+([^;&|\r\n]+)`,
13949
- "gi"
13950
- );
13988
+ const tee = new RegExp(String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(tee)\s+([^;&|\r\n]+)`, "gi");
13951
13989
  let t = tee.exec(normalizedCommand);
13952
13990
  while (t !== null) {
13953
13991
  if (!tokenIsQuoted(t, t[1] ?? "")) targets.push(...shellArgs(t[2] ?? ""));
13954
13992
  t = tee.exec(normalizedCommand);
13955
13993
  }
13956
- const dd = new RegExp(
13957
- String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(dd)\s+([^;&|\r\n]+)`,
13958
- "gi"
13959
- );
13994
+ const dd = new RegExp(String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(dd)\s+([^;&|\r\n]+)`, "gi");
13960
13995
  let d = dd.exec(normalizedCommand);
13961
13996
  while (d !== null) {
13962
13997
  if (!tokenIsQuoted(d, d[1] ?? "")) {
@@ -22651,9 +22686,10 @@ var plugin60 = {
22651
22686
  var todo_listener_default = plugin60;
22652
22687
 
22653
22688
  // src/todo-tracker/index.ts
22654
- import * as fsp from "node:fs/promises";
22655
22689
  import { randomUUID } from "node:crypto";
22690
+ import * as fsp from "node:fs/promises";
22656
22691
  import { atomicWrite as atomicWrite3, ensureDir as ensureDir3 } from "@wrongstack/core/utils";
22692
+ import { nowIso } from "@wrongstack/primitives";
22657
22693
  function deriveFilePath(api) {
22658
22694
  const raw = api.config.extensions?.["todo-tracker"];
22659
22695
  const explicit = typeof raw?.["filePath"] === "string" ? raw["filePath"] : null;
@@ -22698,9 +22734,6 @@ var state56 = {
22698
22734
  /** Most recent mutation for /diag plugins visibility. */
22699
22735
  lastMutation: null
22700
22736
  };
22701
- function nowIso() {
22702
- return (/* @__PURE__ */ new Date()).toISOString();
22703
- }
22704
22737
  function ensureFile() {
22705
22738
  if (!state56.file) {
22706
22739
  state56.file = {
@@ -505,7 +505,7 @@ function stripLauncherAtBoundary(command, launcher, valueTaking) {
505
505
  }
506
506
  return `${command.slice(0, match.index)}${boundary}${after.slice(consumed).replace(/^\s+/, "")}`;
507
507
  }
508
- var XARGS_OPTIONS = String.raw`(?:\s+(?:(?:-[InLsPjeE]|--(?:arg-file|replace|max-args|max-lines|max-chars|max-procs))\s+[^\s]+|-[^\s]+))*`;
508
+ var XARGS_OPTIONS = String.raw`(?:\s+(?:(?:-[InLsPjeE]|--(?:arg-file|replace|max-args|max-lines|max-chars|max-procs))\s+[^\s-][^\s]*|-[^\s]+))*`;
509
509
  var COMMAND_BOUNDARY = String.raw`(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs${XARGS_OPTIONS}\s+)`;
510
510
  function commandRecursivelyDeletes(command) {
511
511
  const stripped = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
@@ -837,19 +837,13 @@ function destructiveTargetsAtDepth(command, depth) {
837
837
  if (destination) targets.push(destination);
838
838
  c = copy.exec(normalizedCommand);
839
839
  }
840
- const tee = new RegExp(
841
- String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(tee)\s+([^;&|\r\n]+)`,
842
- "gi"
843
- );
840
+ const tee = new RegExp(String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(tee)\s+([^;&|\r\n]+)`, "gi");
844
841
  let t = tee.exec(normalizedCommand);
845
842
  while (t !== null) {
846
843
  if (!tokenIsQuoted(t, t[1] ?? "")) targets.push(...shellArgs(t[2] ?? ""));
847
844
  t = tee.exec(normalizedCommand);
848
845
  }
849
- const dd = new RegExp(
850
- String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(dd)\s+([^;&|\r\n]+)`,
851
- "gi"
852
- );
846
+ const dd = new RegExp(String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(dd)\s+([^;&|\r\n]+)`, "gi");
853
847
  let d = dd.exec(normalizedCommand);
854
848
  while (d !== null) {
855
849
  if (!tokenIsQuoted(d, d[1] ?? "")) {
@@ -1,3 +1,32 @@
1
+ /**
2
+ * todo-tracker plugin — Persistent, project-scoped todo backlog that
3
+ * survives across sessions.
4
+ *
5
+ * Why a separate plugin from the built-in `todo` tool?
6
+ * - The built-in `todo` tool mutates `ctx.todos`, which is
7
+ * session-scoped and auto-clears when all items complete.
8
+ * - todo-tracker writes to disk (`~/.wrongstack/projects/<slug>/todo-tracker.json`)
9
+ * and survives across sessions. Items are explicit add/complete;
10
+ * no auto-clear.
11
+ *
12
+ * Use cases:
13
+ * - Backlog of work the user wants to track over days/weeks
14
+ * - Items the LLM noticed but didn't finish — pull them into a fresh
15
+ * session via `todo_tracker_pull` (the LLM then registers them with
16
+ * the session's `ctx.todos` via the built-in `todo` tool)
17
+ * - Per-project scratchpad that survives `wstack resume <id>`
18
+ *
19
+ * Tools registered:
20
+ * - todo_tracker_list : List items, filterable by status/tag/priority
21
+ * - todo_tracker_add : Append a new item
22
+ * - todo_tracker_complete : Mark an item completed (idempotent)
23
+ * - todo_tracker_drop : Mark an item dropped (idempotent)
24
+ * - todo_tracker_remove : Permanently delete by id
25
+ * - todo_tracker_pull : Return pending items for LLM to promote
26
+ * into the session's ctx.todos via the
27
+ * built-in `todo` tool
28
+ * - todo_tracker_status : Counters + last update timestamp
29
+ */
1
30
  import type { Plugin } from '@wrongstack/core/types';
2
31
  declare const plugin: Plugin;
3
32
  export default plugin;
@@ -1,7 +1,8 @@
1
1
  // src/todo-tracker/index.ts
2
- import * as fsp from "node:fs/promises";
3
2
  import { randomUUID } from "node:crypto";
3
+ import * as fsp from "node:fs/promises";
4
4
  import { atomicWrite, ensureDir } from "@wrongstack/core/utils";
5
+ import { nowIso } from "@wrongstack/primitives";
5
6
  function deriveFilePath(api) {
6
7
  const raw = api.config.extensions?.["todo-tracker"];
7
8
  const explicit = typeof raw?.["filePath"] === "string" ? raw["filePath"] : null;
@@ -46,9 +47,6 @@ var state = {
46
47
  /** Most recent mutation for /diag plugins visibility. */
47
48
  lastMutation: null
48
49
  };
49
- function nowIso() {
50
- return (/* @__PURE__ */ new Date()).toISOString();
51
- }
52
50
  function ensureFile() {
53
51
  if (!state.file) {
54
52
  state.file = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/plugins",
3
- "version": "0.309.0",
3
+ "version": "0.310.0",
4
4
  "description": "Official WrongStack collection of focused plugins for code quality, security, observability, planning, and agent coordination",
5
5
  "license": "MIT",
6
6
  "author": "ECOSTACK TECHNOLOGY OÜ",
@@ -300,12 +300,13 @@
300
300
  "devDependencies": {
301
301
  "@types/node": "^26.2.0",
302
302
  "typescript": "^7.0.2",
303
- "vitest": "^4.1.10"
303
+ "vitest": "^4.1.11"
304
304
  },
305
305
  "dependencies": {
306
- "@wrongstack/plugin-sdk": "0.309.0",
307
- "@wrongstack/core": "0.309.0",
308
- "@wrongstack/tools": "0.309.0"
306
+ "@wrongstack/core": "0.310.0",
307
+ "@wrongstack/tools": "0.310.0",
308
+ "@wrongstack/primitives": "0.310.0",
309
+ "@wrongstack/plugin-sdk": "0.310.0"
309
310
  },
310
311
  "scripts": {
311
312
  "build": "node ../../scripts/build-package.mjs",