@wrongstack/plugins 0.282.0 → 0.282.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @wrongstack/plugins
2
2
 
3
3
  First-party plugin collection for [WrongStack](https://github.com/WrongStack/WrongStack).
4
- Sixty-two focused, single-purpose plugins ship in this package. Core safety
4
+ Sixty-three focused, single-purpose plugins ship in this package. Core safety
5
5
  plugins load automatically for every `wstack` session; provider-wire plugins
6
6
  are opt-in because they can change model-call semantics.
7
7
 
@@ -153,7 +153,7 @@ the full lookup chain.
153
153
  "cost-tracker": {
154
154
  "pricingOverrides": {
155
155
  "gpt-4o": { "input": 7, output: 21 },
156
- "claude-3-5-sonnet": { "input": 4, output: 20 }
156
+ "anthropic-test-model": { "input": 4, output: 20 }
157
157
  }
158
158
  }
159
159
  }
@@ -27,7 +27,7 @@ import { Plugin } from '@wrongstack/core';
27
27
  * ```jsonc
28
28
  * {
29
29
  * "enabled": false,
30
- * "escalation": ["claude-sonnet-5", "claude-opus-4-8"],
30
+ * "escalation": ["provider/model-standard", "provider/model-premium"],
31
31
  * "retryablePatterns": ["overload", "rate.?limit", "429", "50[023]", "timeout", "ETIMEDOUT", "ECONNRESET"]
32
32
  * }
33
33
  * ```
@@ -8,17 +8,18 @@ import { Plugin } from '@wrongstack/core';
8
8
  * - branch_guard_status : Show protected branches, mode, and counters.
9
9
  *
10
10
  * Hooks registered:
11
- * - PreToolUse with matcher `bash|git_autocommit`. Inspects the tool
12
- * input for git commit / push / merge / rebase commands (bash) or
13
- * the tool call itself (git_autocommit). If the current branch is
14
- * protected, the call is blocked with a clear reason.
11
+ * - PreToolUse with matcher `bash|git|git_autocommit`. Inspects the tool
12
+ * input for git commit / push / merge commands (bash), structured git
13
+ * operations (git), or the tool call itself (git_autocommit). If the current
14
+ * branch is protected, the call is blocked with a clear reason.
15
15
  *
16
16
  * Config (`config.extensions['branch-guard']`):
17
17
  *
18
18
  * ```jsonc
19
19
  * {
20
+ * "enabled": true, // set false to make the hook a no-op
20
21
  * "branches": ["main", "master"], // protected branch names
21
- * "mode": "block", // "block" | "warn"
22
+ * "mode": "block", // "block" | "warn" | "off"
22
23
  * "blockMerge": true, // also block merges into protected
23
24
  * "blockPush": true, // also block pushes from protected
24
25
  * "blockCommit": true // also block commits on protected
@@ -7,9 +7,11 @@ var state = {
7
7
  blockCount: 0,
8
8
  warnCount: 0,
9
9
  hookUnregister: null,
10
+ configUnregister: null,
10
11
  lastBlock: null
11
12
  };
12
13
  var DEFAULTS = {
14
+ enabled: true,
13
15
  branches: ["main", "master"],
14
16
  mode: "block",
15
17
  blockCommit: true,
@@ -20,14 +22,36 @@ function readConfig(raw) {
20
22
  if (!raw || typeof raw !== "object") return { ...DEFAULTS };
21
23
  const r = raw;
22
24
  const branches = Array.isArray(r["branches"]) ? r["branches"].filter((b) => typeof b === "string") : DEFAULTS.branches;
25
+ const mode = r["mode"] === "warn" ? "warn" : r["mode"] === "off" ? "off" : "block";
23
26
  return {
27
+ enabled: r["enabled"] !== false && mode !== "off",
24
28
  branches: branches.length > 0 ? branches : DEFAULTS.branches,
25
- mode: r["mode"] === "warn" ? "warn" : "block",
29
+ mode,
26
30
  blockCommit: r["blockCommit"] !== false,
27
31
  blockPush: r["blockPush"] !== false,
28
32
  blockMerge: r["blockMerge"] !== false
29
33
  };
30
34
  }
35
+ function readHostConfig(raw) {
36
+ const host = raw && typeof raw === "object" ? raw : {};
37
+ const extensions = host["extensions"];
38
+ const branchGuardOptions = extensions && typeof extensions === "object" ? extensions["branch-guard"] : void 0;
39
+ const cfg = readConfig(branchGuardOptions);
40
+ if (hasDisabledPluginEntry(host["plugins"])) {
41
+ return { ...cfg, enabled: false, mode: "off" };
42
+ }
43
+ return cfg;
44
+ }
45
+ function hasDisabledPluginEntry(raw) {
46
+ if (!Array.isArray(raw)) return false;
47
+ return raw.some((entry) => {
48
+ if (!entry || typeof entry !== "object") return false;
49
+ const r = entry;
50
+ if (r["enabled"] !== false) return false;
51
+ const name = typeof r["name"] === "string" ? r["name"] : "";
52
+ return name === "branch-guard" || name === "@wrongstack/plugins/branch-guard";
53
+ });
54
+ }
31
55
  function getCurrentBranch(cwd) {
32
56
  try {
33
57
  const branch = execSync("git branch --show-current", {
@@ -67,6 +91,16 @@ function detectGitCommand(command) {
67
91
  }
68
92
  return null;
69
93
  }
94
+ function detectStructuredGitCommand(input) {
95
+ const command = input["command"];
96
+ if (command === "commit") {
97
+ if (input["dry_run"] === true) return null;
98
+ return { type: "commit", snippet: "git commit" };
99
+ }
100
+ if (command === "push") return { type: "push", snippet: "git push" };
101
+ if (command === "merge") return { type: "merge", snippet: "git merge" };
102
+ return null;
103
+ }
70
104
  function shouldBlock(op, cfg) {
71
105
  if (op === "commit") return cfg.blockCommit;
72
106
  if (op === "push") return cfg.blockPush;
@@ -117,14 +151,18 @@ var plugin = {
117
151
  state.blockCount = 0;
118
152
  state.warnCount = 0;
119
153
  state.hookUnregister = null;
154
+ state.configUnregister = null;
120
155
  state.lastBlock = null;
121
- const cfg = readConfig(api.config.extensions?.["branch-guard"]);
156
+ let cfg = readHostConfig(api.config);
157
+ state.configUnregister = api.onConfigChange((next) => {
158
+ cfg = readHostConfig(next);
159
+ });
122
160
  const cwd = typeof process.cwd === "function" ? process.cwd() : void 0;
123
- const protectedSet = new Set(cfg.branches);
124
161
  const hook = (input) => {
125
162
  const toolName = input.toolName ?? "";
126
163
  const inp = input.toolInput ?? {};
127
164
  state.invocationCount += 1;
165
+ if (!cfg.enabled || cfg.mode === "off") return;
128
166
  let gitOp = null;
129
167
  if (toolName === "git_autocommit") {
130
168
  if (inp["dry_run"] === true) return;
@@ -133,11 +171,14 @@ var plugin = {
133
171
  const command = inp["command"];
134
172
  if (typeof command !== "string") return;
135
173
  gitOp = detectGitCommand(command);
174
+ } else if (toolName === "git") {
175
+ gitOp = detectStructuredGitCommand(inp);
136
176
  }
137
177
  if (!gitOp) return;
138
178
  if (!shouldBlock(gitOp.type, cfg)) return;
139
179
  const branch = getCurrentBranch(cwd);
140
180
  if (!branch) return;
181
+ const protectedSet = new Set(cfg.branches);
141
182
  if (!protectedSet.has(branch)) return;
142
183
  const when = (/* @__PURE__ */ new Date()).toISOString();
143
184
  const opVerb = gitOp.type === "commit" ? "committing to" : gitOp.type === "push" ? "pushing from" : "merging into";
@@ -174,7 +215,7 @@ var plugin = {
174
215
  \u26A0\uFE0F branch-guard: you are ${opVerb} protected branch '${branch}'. ` + (hasUncommitted ? `You have uncommitted changes \u2014 consider \`git stash\` before switching branches. ` : "") + `Use a feature branch instead. Protected: ${cfg.branches.join(", ")}.`
175
216
  };
176
217
  };
177
- state.hookUnregister = api.registerHook("PreToolUse", "bash|git_autocommit", hook);
218
+ state.hookUnregister = api.registerHook("PreToolUse", "bash|git|git_autocommit", hook);
178
219
  api.tools.register({
179
220
  name: "branch_guard_status",
180
221
  description: "Reports branch-guard state: protected branches, mode, and per-session invocation/block/warn counters.",
@@ -185,6 +226,7 @@ var plugin = {
185
226
  async execute() {
186
227
  return {
187
228
  ok: true,
229
+ enabled: cfg.enabled,
188
230
  branches: cfg.branches,
189
231
  mode: cfg.mode,
190
232
  blockCommit: cfg.blockCommit,
@@ -201,11 +243,19 @@ var plugin = {
201
243
  });
202
244
  api.log.info("branch-guard plugin loaded", {
203
245
  version: "0.1.0",
246
+ enabled: cfg.enabled,
204
247
  branches: cfg.branches,
205
248
  mode: cfg.mode
206
249
  });
207
250
  },
208
251
  teardown(api) {
252
+ if (state.configUnregister) {
253
+ try {
254
+ state.configUnregister();
255
+ } catch {
256
+ }
257
+ state.configUnregister = null;
258
+ }
209
259
  if (state.hookUnregister) {
210
260
  try {
211
261
  state.hookUnregister();
@@ -1,4 +1,4 @@
1
- import { readFileSync, mkdirSync, writeFileSync } from 'fs';
1
+ import * as fs from 'fs';
2
2
  import { dirname, resolve, isAbsolute, relative } from 'path';
3
3
 
4
4
  // src/context-pins/index.ts
@@ -38,7 +38,7 @@ function readConfig(raw) {
38
38
  function loadPins(filePath) {
39
39
  if (!filePath) return { pins: [], nextId: 1 };
40
40
  try {
41
- const raw = JSON.parse(readFileSync(filePath, "utf-8"));
41
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
42
42
  const pins = Array.isArray(raw.pins) ? raw.pins.filter(
43
43
  (p) => !!p && typeof p === "object" && typeof p.id === "string" && typeof p.text === "string"
44
44
  ) : [];
@@ -51,8 +51,8 @@ function loadPins(filePath) {
51
51
  function persistPins(filePath) {
52
52
  if (!filePath) return true;
53
53
  try {
54
- mkdirSync(dirname(filePath), { recursive: true });
55
- writeFileSync(filePath, JSON.stringify({ pins: state.pins, nextId: state.nextId }, null, 2));
54
+ fs.mkdirSync(dirname(filePath), { recursive: true });
55
+ fs.writeFileSync(filePath, JSON.stringify({ pins: state.pins, nextId: state.nextId }, null, 2));
56
56
  return true;
57
57
  } catch {
58
58
  state.persistErrors += 1;
@@ -11,7 +11,7 @@ import { Plugin } from '@wrongstack/core';
11
11
  * "warningThreshold": 80, // percent of budget before warning
12
12
  * "pricingOverrides": { // user-supplied per-model rates (USD/1M tokens)
13
13
  * "gpt-4o": { "input": 5.0, "output": 15.0 },
14
- * "claude-3-5-sonnet": { "input": 3.0, "output": 15.0 }
14
+ * "custom-model": { "input": 3.0, "output": 15.0 }
15
15
  * }
16
16
  * }
17
17
  * ```
@@ -6,9 +6,6 @@ var PRICING = {
6
6
  "gpt-4o": { input: 5, output: 15 },
7
7
  "gpt-4o-mini": { input: 0.15, output: 0.6 },
8
8
  "gpt-4-turbo": { input: 10, output: 30 },
9
- "claude-3-5-sonnet": { input: 3, output: 15 },
10
- "claude-3-5-haiku": { input: 0.8, output: 4 },
11
- "claude-3-opus": { input: 15, output: 75 },
12
9
  "gemini-1.5-pro": { input: 3.5, output: 10.5 },
13
10
  "gemini-1.5-flash": { input: 0.075, output: 0.3 },
14
11
  default: { input: 5, output: 15 }
package/dist/cron.js CHANGED
@@ -4,12 +4,27 @@ var API_VERSION = "^0.1.10";
4
4
  var state = {
5
5
  jobs: /* @__PURE__ */ new Map(),
6
6
  timers: /* @__PURE__ */ new Map(),
7
+ extensionUnregister: null,
7
8
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
8
9
  };
9
10
  function formatNextRun(intervalMs) {
10
11
  const ms = Number.isNaN(intervalMs) || intervalMs <= 0 ? 6e4 : intervalMs;
11
12
  return new Date(Date.now() + ms).toISOString();
12
13
  }
14
+ function clearCronResources() {
15
+ for (const timer of state.timers.values()) {
16
+ clearTimeout(timer);
17
+ }
18
+ state.timers.clear();
19
+ state.jobs.clear();
20
+ if (state.extensionUnregister) {
21
+ try {
22
+ state.extensionUnregister();
23
+ } catch {
24
+ }
25
+ state.extensionUnregister = null;
26
+ }
27
+ }
13
28
  var plugin = {
14
29
  name: "cron",
15
30
  version: "0.1.0",
@@ -30,8 +45,7 @@ var plugin = {
30
45
  }
31
46
  },
32
47
  setup(api) {
33
- state.jobs.clear();
34
- state.timers.clear();
48
+ clearCronResources();
35
49
  state.createdAt = (/* @__PURE__ */ new Date()).toISOString();
36
50
  const maxConcurrent = api.config.extensions?.["cron"]?.["maxConcurrentJobs"] ?? 5;
37
51
  function scheduleNextRun(name) {
@@ -64,7 +78,7 @@ var plugin = {
64
78
  }
65
79
  state.jobs.delete(name);
66
80
  }
67
- api.extensions.register({
81
+ state.extensionUnregister = api.extensions.register({
68
82
  name: "cron-iteration-hooks",
69
83
  owner: "cron",
70
84
  beforeIteration: async (_ctx, _idx) => {
@@ -216,11 +230,7 @@ var plugin = {
216
230
  api.log.info("cron plugin loaded", { version: "0.1.0", maxConcurrent });
217
231
  },
218
232
  teardown(api) {
219
- for (const timer of state.timers.values()) {
220
- clearTimeout(timer);
221
- }
222
- state.timers.clear();
223
- state.jobs.clear();
233
+ clearCronResources();
224
234
  api.log.info("cron plugin unloaded");
225
235
  }
226
236
  };
@@ -27,6 +27,8 @@ var state = {
27
27
  coveredSkipCount: 0,
28
28
  /** Hook handle for teardown. */
29
29
  hookUnregister: null,
30
+ /** Cross-plugin event listener handle for teardown. */
31
+ patternUnregister: null,
30
32
  /** Last format result — surfaced by health() + status tool. */
31
33
  lastResult: null
32
34
  };
@@ -47,6 +49,22 @@ function readConfig(raw) {
47
49
  };
48
50
  }
49
51
  var recentlyCovered = /* @__PURE__ */ new Map();
52
+ function clearRegistrations() {
53
+ if (state.hookUnregister) {
54
+ try {
55
+ state.hookUnregister();
56
+ } catch {
57
+ }
58
+ state.hookUnregister = null;
59
+ }
60
+ if (state.patternUnregister) {
61
+ try {
62
+ state.patternUnregister();
63
+ } catch {
64
+ }
65
+ state.patternUnregister = null;
66
+ }
67
+ }
50
68
  function evictExpired(ttlMs) {
51
69
  const cutoff = Date.now() - ttlMs;
52
70
  for (const [path, ts] of recentlyCovered) {
@@ -129,12 +147,12 @@ var plugin = {
129
147
  }
130
148
  },
131
149
  setup(api) {
150
+ clearRegistrations();
132
151
  state.invocationCount = 0;
133
152
  state.formattedCount = 0;
134
153
  state.cleanCount = 0;
135
154
  state.errorCount = 0;
136
155
  state.coveredSkipCount = 0;
137
- state.hookUnregister = null;
138
156
  state.lastResult = null;
139
157
  recentlyCovered.clear();
140
158
  const cfg = readConfig(api.config.extensions?.["format-on-save"]);
@@ -203,12 +221,15 @@ var plugin = {
203
221
  return;
204
222
  };
205
223
  state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
206
- api.onPattern("import-organizer:done", (_eventName, payload) => {
207
- const p = payload ?? {};
208
- if (typeof p.path !== "string" || p.path.length === 0) return;
209
- recentlyCovered.set(p.path, Date.now());
210
- api.metrics.counter("covered_notice");
211
- });
224
+ state.patternUnregister = api.onPattern(
225
+ "import-organizer:done",
226
+ (_eventName, payload) => {
227
+ const p = payload ?? {};
228
+ if (typeof p.path !== "string" || p.path.length === 0) return;
229
+ recentlyCovered.set(p.path, Date.now());
230
+ api.metrics.counter("covered_notice");
231
+ }
232
+ );
212
233
  api.tools.register({
213
234
  name: "format_on_save_status",
214
235
  description: "Reports format-on-save state: biome availability, and per-session formatted/clean/error/skipped counters.",
@@ -242,13 +263,7 @@ var plugin = {
242
263
  });
243
264
  },
244
265
  teardown(api) {
245
- if (state.hookUnregister) {
246
- try {
247
- state.hookUnregister();
248
- } catch {
249
- }
250
- state.hookUnregister = null;
251
- }
266
+ clearRegistrations();
252
267
  const final = {
253
268
  invocations: state.invocationCount,
254
269
  formatted: state.formattedCount,
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { dirname, resolve, extname, isAbsolute, relative, join, basename } from 'path';
2
2
  import { execSync, execFileSync, spawn } from 'child_process';
3
+ import * as fs from 'fs';
3
4
  import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, watch, readdirSync, mkdtempSync, rmSync } from 'fs';
4
5
  import { expectDefined } from '@wrongstack/core';
5
6
  import { tmpdir } from 'os';
@@ -844,9 +845,11 @@ var state4 = {
844
845
  blockCount: 0,
845
846
  warnCount: 0,
846
847
  hookUnregister: null,
848
+ configUnregister: null,
847
849
  lastBlock: null
848
850
  };
849
851
  var DEFAULTS2 = {
852
+ enabled: true,
850
853
  branches: ["main", "master"],
851
854
  mode: "block",
852
855
  blockCommit: true,
@@ -857,14 +860,36 @@ function readConfig3(raw) {
857
860
  if (!raw || typeof raw !== "object") return { ...DEFAULTS2 };
858
861
  const r = raw;
859
862
  const branches = Array.isArray(r["branches"]) ? r["branches"].filter((b) => typeof b === "string") : DEFAULTS2.branches;
863
+ const mode = r["mode"] === "warn" ? "warn" : r["mode"] === "off" ? "off" : "block";
860
864
  return {
865
+ enabled: r["enabled"] !== false && mode !== "off",
861
866
  branches: branches.length > 0 ? branches : DEFAULTS2.branches,
862
- mode: r["mode"] === "warn" ? "warn" : "block",
867
+ mode,
863
868
  blockCommit: r["blockCommit"] !== false,
864
869
  blockPush: r["blockPush"] !== false,
865
870
  blockMerge: r["blockMerge"] !== false
866
871
  };
867
872
  }
873
+ function readHostConfig(raw) {
874
+ const host = raw && typeof raw === "object" ? raw : {};
875
+ const extensions = host["extensions"];
876
+ const branchGuardOptions = extensions && typeof extensions === "object" ? extensions["branch-guard"] : void 0;
877
+ const cfg = readConfig3(branchGuardOptions);
878
+ if (hasDisabledPluginEntry(host["plugins"])) {
879
+ return { ...cfg, enabled: false, mode: "off" };
880
+ }
881
+ return cfg;
882
+ }
883
+ function hasDisabledPluginEntry(raw) {
884
+ if (!Array.isArray(raw)) return false;
885
+ return raw.some((entry) => {
886
+ if (!entry || typeof entry !== "object") return false;
887
+ const r = entry;
888
+ if (r["enabled"] !== false) return false;
889
+ const name = typeof r["name"] === "string" ? r["name"] : "";
890
+ return name === "branch-guard" || name === "@wrongstack/plugins/branch-guard";
891
+ });
892
+ }
868
893
  function getCurrentBranch(cwd) {
869
894
  try {
870
895
  const branch = execSync("git branch --show-current", {
@@ -904,6 +929,16 @@ function detectGitCommand(command) {
904
929
  }
905
930
  return null;
906
931
  }
932
+ function detectStructuredGitCommand(input) {
933
+ const command = input["command"];
934
+ if (command === "commit") {
935
+ if (input["dry_run"] === true) return null;
936
+ return { type: "commit", snippet: "git commit" };
937
+ }
938
+ if (command === "push") return { type: "push", snippet: "git push" };
939
+ if (command === "merge") return { type: "merge", snippet: "git merge" };
940
+ return null;
941
+ }
907
942
  function shouldBlock(op, cfg) {
908
943
  if (op === "commit") return cfg.blockCommit;
909
944
  if (op === "push") return cfg.blockPush;
@@ -954,14 +989,18 @@ var plugin4 = {
954
989
  state4.blockCount = 0;
955
990
  state4.warnCount = 0;
956
991
  state4.hookUnregister = null;
992
+ state4.configUnregister = null;
957
993
  state4.lastBlock = null;
958
- const cfg = readConfig3(api.config.extensions?.["branch-guard"]);
994
+ let cfg = readHostConfig(api.config);
995
+ state4.configUnregister = api.onConfigChange((next) => {
996
+ cfg = readHostConfig(next);
997
+ });
959
998
  const cwd = typeof process.cwd === "function" ? process.cwd() : void 0;
960
- const protectedSet = new Set(cfg.branches);
961
999
  const hook = (input) => {
962
1000
  const toolName = input.toolName ?? "";
963
1001
  const inp = input.toolInput ?? {};
964
1002
  state4.invocationCount += 1;
1003
+ if (!cfg.enabled || cfg.mode === "off") return;
965
1004
  let gitOp = null;
966
1005
  if (toolName === "git_autocommit") {
967
1006
  if (inp["dry_run"] === true) return;
@@ -970,11 +1009,14 @@ var plugin4 = {
970
1009
  const command = inp["command"];
971
1010
  if (typeof command !== "string") return;
972
1011
  gitOp = detectGitCommand(command);
1012
+ } else if (toolName === "git") {
1013
+ gitOp = detectStructuredGitCommand(inp);
973
1014
  }
974
1015
  if (!gitOp) return;
975
1016
  if (!shouldBlock(gitOp.type, cfg)) return;
976
1017
  const branch = getCurrentBranch(cwd);
977
1018
  if (!branch) return;
1019
+ const protectedSet = new Set(cfg.branches);
978
1020
  if (!protectedSet.has(branch)) return;
979
1021
  const when = (/* @__PURE__ */ new Date()).toISOString();
980
1022
  const opVerb = gitOp.type === "commit" ? "committing to" : gitOp.type === "push" ? "pushing from" : "merging into";
@@ -1011,7 +1053,7 @@ var plugin4 = {
1011
1053
  \u26A0\uFE0F branch-guard: you are ${opVerb} protected branch '${branch}'. ` + (hasUncommitted ? `You have uncommitted changes \u2014 consider \`git stash\` before switching branches. ` : "") + `Use a feature branch instead. Protected: ${cfg.branches.join(", ")}.`
1012
1054
  };
1013
1055
  };
1014
- state4.hookUnregister = api.registerHook("PreToolUse", "bash|git_autocommit", hook);
1056
+ state4.hookUnregister = api.registerHook("PreToolUse", "bash|git|git_autocommit", hook);
1015
1057
  api.tools.register({
1016
1058
  name: "branch_guard_status",
1017
1059
  description: "Reports branch-guard state: protected branches, mode, and per-session invocation/block/warn counters.",
@@ -1022,6 +1064,7 @@ var plugin4 = {
1022
1064
  async execute() {
1023
1065
  return {
1024
1066
  ok: true,
1067
+ enabled: cfg.enabled,
1025
1068
  branches: cfg.branches,
1026
1069
  mode: cfg.mode,
1027
1070
  blockCommit: cfg.blockCommit,
@@ -1038,11 +1081,19 @@ var plugin4 = {
1038
1081
  });
1039
1082
  api.log.info("branch-guard plugin loaded", {
1040
1083
  version: "0.1.0",
1084
+ enabled: cfg.enabled,
1041
1085
  branches: cfg.branches,
1042
1086
  mode: cfg.mode
1043
1087
  });
1044
1088
  },
1045
1089
  teardown(api) {
1090
+ if (state4.configUnregister) {
1091
+ try {
1092
+ state4.configUnregister();
1093
+ } catch {
1094
+ }
1095
+ state4.configUnregister = null;
1096
+ }
1046
1097
  if (state4.hookUnregister) {
1047
1098
  try {
1048
1099
  state4.hookUnregister();
@@ -2535,7 +2586,7 @@ function readConfig8(raw) {
2535
2586
  function loadPins(filePath) {
2536
2587
  if (!filePath) return { pins: [], nextId: 1 };
2537
2588
  try {
2538
- const raw = JSON.parse(readFileSync(filePath, "utf-8"));
2589
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2539
2590
  const pins = Array.isArray(raw.pins) ? raw.pins.filter(
2540
2591
  (p) => !!p && typeof p === "object" && typeof p.id === "string" && typeof p.text === "string"
2541
2592
  ) : [];
@@ -2548,8 +2599,8 @@ function loadPins(filePath) {
2548
2599
  function persistPins(filePath) {
2549
2600
  if (!filePath) return true;
2550
2601
  try {
2551
- mkdirSync(dirname(filePath), { recursive: true });
2552
- writeFileSync(filePath, JSON.stringify({ pins: state9.pins, nextId: state9.nextId }, null, 2));
2602
+ fs.mkdirSync(dirname(filePath), { recursive: true });
2603
+ fs.writeFileSync(filePath, JSON.stringify({ pins: state9.pins, nextId: state9.nextId }, null, 2));
2553
2604
  return true;
2554
2605
  } catch {
2555
2606
  state9.persistErrors += 1;
@@ -2747,9 +2798,6 @@ var PRICING = {
2747
2798
  "gpt-4o": { input: 5, output: 15 },
2748
2799
  "gpt-4o-mini": { input: 0.15, output: 0.6 },
2749
2800
  "gpt-4-turbo": { input: 10, output: 30 },
2750
- "claude-3-5-sonnet": { input: 3, output: 15 },
2751
- "claude-3-5-haiku": { input: 0.8, output: 4 },
2752
- "claude-3-opus": { input: 15, output: 75 },
2753
2801
  "gemini-1.5-pro": { input: 3.5, output: 10.5 },
2754
2802
  "gemini-1.5-flash": { input: 0.075, output: 0.3 },
2755
2803
  default: { input: 5, output: 15 }
@@ -3110,12 +3158,27 @@ var API_VERSION5 = "^0.1.10";
3110
3158
  var state10 = {
3111
3159
  jobs: /* @__PURE__ */ new Map(),
3112
3160
  timers: /* @__PURE__ */ new Map(),
3161
+ extensionUnregister: null,
3113
3162
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3114
3163
  };
3115
3164
  function formatNextRun(intervalMs) {
3116
3165
  const ms = Number.isNaN(intervalMs) || intervalMs <= 0 ? 6e4 : intervalMs;
3117
3166
  return new Date(Date.now() + ms).toISOString();
3118
3167
  }
3168
+ function clearCronResources() {
3169
+ for (const timer of state10.timers.values()) {
3170
+ clearTimeout(timer);
3171
+ }
3172
+ state10.timers.clear();
3173
+ state10.jobs.clear();
3174
+ if (state10.extensionUnregister) {
3175
+ try {
3176
+ state10.extensionUnregister();
3177
+ } catch {
3178
+ }
3179
+ state10.extensionUnregister = null;
3180
+ }
3181
+ }
3119
3182
  var plugin11 = {
3120
3183
  name: "cron",
3121
3184
  version: "0.1.0",
@@ -3136,8 +3199,7 @@ var plugin11 = {
3136
3199
  }
3137
3200
  },
3138
3201
  setup(api) {
3139
- state10.jobs.clear();
3140
- state10.timers.clear();
3202
+ clearCronResources();
3141
3203
  state10.createdAt = (/* @__PURE__ */ new Date()).toISOString();
3142
3204
  const maxConcurrent = api.config.extensions?.["cron"]?.["maxConcurrentJobs"] ?? 5;
3143
3205
  function scheduleNextRun(name) {
@@ -3170,7 +3232,7 @@ var plugin11 = {
3170
3232
  }
3171
3233
  state10.jobs.delete(name);
3172
3234
  }
3173
- api.extensions.register({
3235
+ state10.extensionUnregister = api.extensions.register({
3174
3236
  name: "cron-iteration-hooks",
3175
3237
  owner: "cron",
3176
3238
  beforeIteration: async (_ctx, _idx) => {
@@ -3322,11 +3384,7 @@ var plugin11 = {
3322
3384
  api.log.info("cron plugin loaded", { version: "0.1.0", maxConcurrent });
3323
3385
  },
3324
3386
  teardown(api) {
3325
- for (const timer of state10.timers.values()) {
3326
- clearTimeout(timer);
3327
- }
3328
- state10.timers.clear();
3329
- state10.jobs.clear();
3387
+ clearCronResources();
3330
3388
  api.log.info("cron plugin unloaded");
3331
3389
  }
3332
3390
  };
@@ -4617,6 +4675,8 @@ var state14 = {
4617
4675
  coveredSkipCount: 0,
4618
4676
  /** Hook handle for teardown. */
4619
4677
  hookUnregister: null,
4678
+ /** Cross-plugin event listener handle for teardown. */
4679
+ patternUnregister: null,
4620
4680
  /** Last format result — surfaced by health() + status tool. */
4621
4681
  lastResult: null
4622
4682
  };
@@ -4637,6 +4697,22 @@ function readConfig12(raw) {
4637
4697
  };
4638
4698
  }
4639
4699
  var recentlyCovered = /* @__PURE__ */ new Map();
4700
+ function clearRegistrations() {
4701
+ if (state14.hookUnregister) {
4702
+ try {
4703
+ state14.hookUnregister();
4704
+ } catch {
4705
+ }
4706
+ state14.hookUnregister = null;
4707
+ }
4708
+ if (state14.patternUnregister) {
4709
+ try {
4710
+ state14.patternUnregister();
4711
+ } catch {
4712
+ }
4713
+ state14.patternUnregister = null;
4714
+ }
4715
+ }
4640
4716
  function evictExpired(ttlMs) {
4641
4717
  const cutoff = Date.now() - ttlMs;
4642
4718
  for (const [path, ts] of recentlyCovered) {
@@ -4719,12 +4795,12 @@ var plugin16 = {
4719
4795
  }
4720
4796
  },
4721
4797
  setup(api) {
4798
+ clearRegistrations();
4722
4799
  state14.invocationCount = 0;
4723
4800
  state14.formattedCount = 0;
4724
4801
  state14.cleanCount = 0;
4725
4802
  state14.errorCount = 0;
4726
4803
  state14.coveredSkipCount = 0;
4727
- state14.hookUnregister = null;
4728
4804
  state14.lastResult = null;
4729
4805
  recentlyCovered.clear();
4730
4806
  const cfg = readConfig12(api.config.extensions?.["format-on-save"]);
@@ -4793,12 +4869,15 @@ var plugin16 = {
4793
4869
  return;
4794
4870
  };
4795
4871
  state14.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
4796
- api.onPattern("import-organizer:done", (_eventName, payload) => {
4797
- const p = payload ?? {};
4798
- if (typeof p.path !== "string" || p.path.length === 0) return;
4799
- recentlyCovered.set(p.path, Date.now());
4800
- api.metrics.counter("covered_notice");
4801
- });
4872
+ state14.patternUnregister = api.onPattern(
4873
+ "import-organizer:done",
4874
+ (_eventName, payload) => {
4875
+ const p = payload ?? {};
4876
+ if (typeof p.path !== "string" || p.path.length === 0) return;
4877
+ recentlyCovered.set(p.path, Date.now());
4878
+ api.metrics.counter("covered_notice");
4879
+ }
4880
+ );
4802
4881
  api.tools.register({
4803
4882
  name: "format_on_save_status",
4804
4883
  description: "Reports format-on-save state: biome availability, and per-session formatted/clean/error/skipped counters.",
@@ -4832,13 +4911,7 @@ var plugin16 = {
4832
4911
  });
4833
4912
  },
4834
4913
  teardown(api) {
4835
- if (state14.hookUnregister) {
4836
- try {
4837
- state14.hookUnregister();
4838
- } catch {
4839
- }
4840
- state14.hookUnregister = null;
4841
- }
4914
+ clearRegistrations();
4842
4915
  const final = {
4843
4916
  invocations: state14.invocationCount,
4844
4917
  formatted: state14.formattedCount,
@@ -8018,8 +8091,20 @@ var path_guard_default = plugin26;
8018
8091
  // src/plugin-stack-observer/index.ts
8019
8092
  var state24 = {
8020
8093
  wraps: [],
8021
- contributions: 0
8094
+ contributions: 0,
8095
+ patternUnregister: null
8022
8096
  };
8097
+ function clearObserverState() {
8098
+ if (state24.patternUnregister) {
8099
+ try {
8100
+ state24.patternUnregister();
8101
+ } catch {
8102
+ }
8103
+ state24.patternUnregister = null;
8104
+ }
8105
+ state24.wraps = [];
8106
+ state24.contributions = 0;
8107
+ }
8023
8108
  var PLUGIN = {
8024
8109
  name: "plugin-stack-observer",
8025
8110
  version: "0.1.0",
@@ -8047,26 +8132,28 @@ var PLUGIN = {
8047
8132
  }
8048
8133
  },
8049
8134
  setup(api) {
8050
- state24.wraps = [];
8051
- state24.contributions = 0;
8135
+ clearObserverState();
8052
8136
  const cfg = readConfig22(api.config.extensions?.["plugin-stack-observer"]);
8053
8137
  if (!cfg.enabled) {
8054
8138
  api.log.info("plugin-stack-observer loaded (disabled)");
8055
8139
  return;
8056
8140
  }
8057
- api.onPattern("provider.wrap:loaded", (_eventName, payload) => {
8058
- const p = payload ?? {};
8059
- if (typeof p.plugin !== "string" || p.plugin.length === 0) return;
8060
- const wraps = Array.isArray(p.wraps) ? p.wraps.filter((w) => typeof w === "string") : [];
8061
- const kind = typeof p.kind === "string" ? p.kind : "unknown";
8062
- state24.wraps.push({
8063
- plugin: p.plugin,
8064
- kind,
8065
- wraps,
8066
- loadedAt: Date.now()
8067
- });
8068
- api.metrics.counter("wrap_loaded");
8069
- });
8141
+ state24.patternUnregister = api.onPattern(
8142
+ "provider.wrap:loaded",
8143
+ (_eventName, payload) => {
8144
+ const p = payload ?? {};
8145
+ if (typeof p.plugin !== "string" || p.plugin.length === 0) return;
8146
+ const wraps = Array.isArray(p.wraps) ? p.wraps.filter((w) => typeof w === "string") : [];
8147
+ const kind = typeof p.kind === "string" ? p.kind : "unknown";
8148
+ state24.wraps.push({
8149
+ plugin: p.plugin,
8150
+ kind,
8151
+ wraps,
8152
+ loadedAt: Date.now()
8153
+ });
8154
+ api.metrics.counter("wrap_loaded");
8155
+ }
8156
+ );
8070
8157
  if (cfg.injectIntoSystemPrompt) {
8071
8158
  api.registerSystemPromptContributor(async () => {
8072
8159
  if (state24.wraps.length === 0) return [];
@@ -8114,8 +8201,7 @@ Any failure or latency above is attributable to one of the above.`
8114
8201
  wrapCount: state24.wraps.length,
8115
8202
  contributions: state24.contributions
8116
8203
  };
8117
- state24.wraps = [];
8118
- state24.contributions = 0;
8204
+ clearObserverState();
8119
8205
  api.log.info("plugin-stack-observer: teardown complete", { final });
8120
8206
  },
8121
8207
  async health() {
@@ -1,8 +1,20 @@
1
1
  // src/plugin-stack-observer/index.ts
2
2
  var state = {
3
3
  wraps: [],
4
- contributions: 0
4
+ contributions: 0,
5
+ patternUnregister: null
5
6
  };
7
+ function clearObserverState() {
8
+ if (state.patternUnregister) {
9
+ try {
10
+ state.patternUnregister();
11
+ } catch {
12
+ }
13
+ state.patternUnregister = null;
14
+ }
15
+ state.wraps = [];
16
+ state.contributions = 0;
17
+ }
6
18
  var PLUGIN = {
7
19
  name: "plugin-stack-observer",
8
20
  version: "0.1.0",
@@ -30,26 +42,28 @@ var PLUGIN = {
30
42
  }
31
43
  },
32
44
  setup(api) {
33
- state.wraps = [];
34
- state.contributions = 0;
45
+ clearObserverState();
35
46
  const cfg = readConfig(api.config.extensions?.["plugin-stack-observer"]);
36
47
  if (!cfg.enabled) {
37
48
  api.log.info("plugin-stack-observer loaded (disabled)");
38
49
  return;
39
50
  }
40
- api.onPattern("provider.wrap:loaded", (_eventName, payload) => {
41
- const p = payload ?? {};
42
- if (typeof p.plugin !== "string" || p.plugin.length === 0) return;
43
- const wraps = Array.isArray(p.wraps) ? p.wraps.filter((w) => typeof w === "string") : [];
44
- const kind = typeof p.kind === "string" ? p.kind : "unknown";
45
- state.wraps.push({
46
- plugin: p.plugin,
47
- kind,
48
- wraps,
49
- loadedAt: Date.now()
50
- });
51
- api.metrics.counter("wrap_loaded");
52
- });
51
+ state.patternUnregister = api.onPattern(
52
+ "provider.wrap:loaded",
53
+ (_eventName, payload) => {
54
+ const p = payload ?? {};
55
+ if (typeof p.plugin !== "string" || p.plugin.length === 0) return;
56
+ const wraps = Array.isArray(p.wraps) ? p.wraps.filter((w) => typeof w === "string") : [];
57
+ const kind = typeof p.kind === "string" ? p.kind : "unknown";
58
+ state.wraps.push({
59
+ plugin: p.plugin,
60
+ kind,
61
+ wraps,
62
+ loadedAt: Date.now()
63
+ });
64
+ api.metrics.counter("wrap_loaded");
65
+ }
66
+ );
53
67
  if (cfg.injectIntoSystemPrompt) {
54
68
  api.registerSystemPromptContributor(async () => {
55
69
  if (state.wraps.length === 0) return [];
@@ -97,8 +111,7 @@ Any failure or latency above is attributable to one of the above.`
97
111
  wrapCount: state.wraps.length,
98
112
  contributions: state.contributions
99
113
  };
100
- state.wraps = [];
101
- state.contributions = 0;
114
+ clearObserverState();
102
115
  api.log.info("plugin-stack-observer: teardown complete", { final });
103
116
  },
104
117
  async health() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/plugins",
3
- "version": "0.282.0",
3
+ "version": "0.282.1",
4
4
  "description": "Official WrongStack plugin collection — 62 focused, single-purpose plugins for code quality, security, observability, planning, and agent coordination",
5
5
  "license": "MIT",
6
6
  "author": "ECOSTACK TECHNOLOGY OÜ",
@@ -275,8 +275,8 @@
275
275
  "vitest": "^4.1.9"
276
276
  },
277
277
  "dependencies": {
278
- "@wrongstack/core": "0.282.0",
279
- "@wrongstack/tools": "0.282.0"
278
+ "@wrongstack/tools": "0.282.1",
279
+ "@wrongstack/core": "0.282.1"
280
280
  },
281
281
  "scripts": {
282
282
  "build": "tsup",