@wrongstack/plugins 0.295.0 → 0.296.2

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.
Files changed (110) hide show
  1. package/dist/accessibility-auditor/index.d.ts.map +1 -1
  2. package/dist/accessibility-auditor.js +31 -3
  3. package/dist/agent-handoff/index.d.ts.map +1 -1
  4. package/dist/agent-handoff.js +30 -1
  5. package/dist/auto-doc/index.d.ts.map +1 -1
  6. package/dist/auto-doc.js +3 -1
  7. package/dist/auto-i18n-extractor.js +5 -0
  8. package/dist/branch-guard/index.d.ts.map +1 -1
  9. package/dist/branch-guard.js +116 -3
  10. package/dist/changelog-writer/index.d.ts.map +1 -1
  11. package/dist/changelog-writer.js +137 -3
  12. package/dist/checkpoint/index.d.ts +2 -0
  13. package/dist/checkpoint/index.d.ts.map +1 -1
  14. package/dist/checkpoint.js +34 -8
  15. package/dist/code-metrics.js +5 -0
  16. package/dist/commit-validator/index.d.ts.map +1 -1
  17. package/dist/commit-validator.js +35 -8
  18. package/dist/context-pins/index.d.ts.map +1 -1
  19. package/dist/context-pins.js +6 -5
  20. package/dist/cost-tracker/index.d.ts.map +1 -1
  21. package/dist/cost-tracker.js +101 -1
  22. package/dist/cron/index.d.ts.map +1 -1
  23. package/dist/cron.js +1 -0
  24. package/dist/dead-code-detector/index.d.ts.map +1 -1
  25. package/dist/dead-code-detector.js +9 -1
  26. package/dist/dependency-vulnerability-gate/index.d.ts.map +1 -1
  27. package/dist/dependency-vulnerability-gate.js +43 -8
  28. package/dist/diff-summary/index.d.ts.map +1 -1
  29. package/dist/diff-summary.js +112 -2
  30. package/dist/doc-sync-guard.js +17 -1
  31. package/dist/duplicate-code-detector/index.d.ts.map +1 -1
  32. package/dist/duplicate-code-detector.js +102 -2
  33. package/dist/feature-flag-tracker/index.d.ts.map +1 -1
  34. package/dist/feature-flag-tracker.js +30 -4
  35. package/dist/file-watcher/index.d.ts.map +1 -1
  36. package/dist/file-watcher.js +6 -7
  37. package/dist/format-on-save/index.d.ts +2 -0
  38. package/dist/format-on-save/index.d.ts.map +1 -1
  39. package/dist/format-on-save.js +288 -59
  40. package/dist/git-autocommit/index.d.ts +12 -0
  41. package/dist/git-autocommit/index.d.ts.map +1 -1
  42. package/dist/git-autocommit.js +58 -4
  43. package/dist/import-organizer/index.d.ts.map +1 -1
  44. package/dist/import-organizer.js +152 -21
  45. package/dist/index.js +1413 -590
  46. package/dist/injection-shield/index.d.ts +16 -0
  47. package/dist/injection-shield/index.d.ts.map +1 -1
  48. package/dist/injection-shield.js +12 -4
  49. package/dist/interface-contract-guard/index.d.ts.map +1 -1
  50. package/dist/interface-contract-guard.js +60 -26
  51. package/dist/knowledge-graph/index.d.ts.map +1 -1
  52. package/dist/knowledge-graph.js +7 -6
  53. package/dist/lint-gate/index.d.ts.map +1 -1
  54. package/dist/lint-gate.js +115 -2
  55. package/dist/loop-breaker/index.d.ts.map +1 -1
  56. package/dist/loop-breaker.js +15 -5
  57. package/dist/migration-planner.js +15 -1
  58. package/dist/notify-hub/index.d.ts.map +1 -1
  59. package/dist/notify-hub.js +41 -4
  60. package/dist/pr-drafter/index.d.ts.map +1 -1
  61. package/dist/pr-drafter.js +20 -1
  62. package/dist/prompt-firewall/index.d.ts.map +1 -1
  63. package/dist/prompt-firewall.js +140 -11
  64. package/dist/refactor-suggester/index.d.ts.map +1 -1
  65. package/dist/refactor-suggester.js +119 -4
  66. package/dist/runtime/bounded-map.d.ts +86 -0
  67. package/dist/runtime/bounded-map.d.ts.map +1 -0
  68. package/dist/runtime/credential-patterns.d.ts +42 -0
  69. package/dist/runtime/credential-patterns.d.ts.map +1 -0
  70. package/dist/runtime/handles.d.ts +46 -0
  71. package/dist/runtime/handles.d.ts.map +1 -0
  72. package/dist/runtime/index.d.ts +4 -0
  73. package/dist/runtime/index.d.ts.map +1 -1
  74. package/dist/runtime/local-bin.d.ts +120 -0
  75. package/dist/runtime/local-bin.d.ts.map +1 -0
  76. package/dist/runtime/safe-json.d.ts +25 -0
  77. package/dist/runtime/safe-json.d.ts.map +1 -0
  78. package/dist/runtime.js +309 -22
  79. package/dist/schema-evolution-guard.js +5 -0
  80. package/dist/secret-scanner/index.d.ts.map +1 -1
  81. package/dist/secret-scanner.js +194 -50
  82. package/dist/security-hotspot-scanner/index.d.ts.map +1 -1
  83. package/dist/security-hotspot-scanner.js +132 -1
  84. package/dist/semantic-search-indexer/index.d.ts.map +1 -1
  85. package/dist/semantic-search-indexer.js +7 -1
  86. package/dist/semver-bump/index.d.ts.map +1 -1
  87. package/dist/semver-bump.js +21 -2
  88. package/dist/session-recap/index.d.ts.map +1 -1
  89. package/dist/session-recap.js +18 -1
  90. package/dist/smart-rename/index.d.ts +2 -0
  91. package/dist/smart-rename/index.d.ts.map +1 -1
  92. package/dist/smart-rename.js +16 -2
  93. package/dist/spec-linker/index.d.ts.map +1 -1
  94. package/dist/spec-linker.js +19 -2
  95. package/dist/template-engine/index.d.ts.map +1 -1
  96. package/dist/template-engine.js +45 -3
  97. package/dist/test-coverage-gate.js +17 -1
  98. package/dist/test-runner-gate/index.d.ts.map +1 -1
  99. package/dist/test-runner-gate.js +142 -9
  100. package/dist/todo-listener/index.d.ts.map +1 -1
  101. package/dist/todo-listener.js +42 -2
  102. package/dist/todo-tracker/index.d.ts.map +1 -1
  103. package/dist/todo-tracker.js +3 -4
  104. package/dist/token-budget/index.d.ts.map +1 -1
  105. package/dist/token-budget.js +31 -19
  106. package/dist/token-throttle/index.d.ts.map +1 -1
  107. package/dist/token-throttle.js +17 -2
  108. package/dist/type-gate/index.d.ts.map +1 -1
  109. package/dist/type-gate.js +108 -17
  110. package/package.json +3 -3
@@ -1,6 +1,23 @@
1
1
  // src/spec-linker/index.ts
2
2
  import * as fs from "node:fs/promises";
3
3
 
4
+ // src/runtime/local-bin.ts
5
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
6
+
7
+ // src/runtime/handles.ts
8
+ function releaseHandle(off) {
9
+ if (off) {
10
+ try {
11
+ off();
12
+ } catch {
13
+ }
14
+ }
15
+ return null;
16
+ }
17
+
18
+ // src/runtime/index.ts
19
+ var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
20
+
4
21
  // src/manifest/index.ts
5
22
  var OFFICIAL_PLUGIN_NAMES = [
6
23
  "agent-handoff",
@@ -330,8 +347,8 @@ var plugin = {
330
347
  state.skippedNonMd = 0;
331
348
  state.readErrorCount = 0;
332
349
  state.autoFixApplied = 0;
333
- state.postHookUnregister = null;
334
- state.preHookUnregister = null;
350
+ state.postHookUnregister = releaseHandle(state.postHookUnregister);
351
+ state.preHookUnregister = releaseHandle(state.preHookUnregister);
335
352
  const cfg = readConfig(api.config.extensions?.["spec-linker"]);
336
353
  const postHook = async (input) => {
337
354
  if (!cfg.enabled) return;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/template-engine/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAsHrD,QAAA,MAAM,MAAM,EAAE,MA4Sb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/template-engine/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AA0IrD,QAAA,MAAM,MAAM,EAAE,MA+Sb,CAAC;eAEa,MAAM"}
@@ -1,8 +1,45 @@
1
1
  // src/template-engine/index.ts
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
- import { isAbsolute } from "node:path";
3
+ import { isAbsolute as isAbsolute2 } from "node:path";
4
+
5
+ // src/runtime/index.ts
6
+ import { basename, extname, isAbsolute, relative, resolve } from "node:path";
7
+
8
+ // src/runtime/local-bin.ts
9
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
10
+
11
+ // src/runtime/handles.ts
12
+ function releaseHandle(off) {
13
+ if (off) {
14
+ try {
15
+ off();
16
+ } catch {
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+
22
+ // src/runtime/index.ts
23
+ var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
24
+ function hasLeadingDash(arg) {
25
+ return arg.length > 0 && arg.startsWith("-");
26
+ }
27
+ function withinProjectPath(projectRoot, candidate) {
28
+ if (candidate.length === 0 || candidate.length > 4096) return false;
29
+ if (hasLeadingDash(candidate)) return false;
30
+ const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(projectRoot, candidate);
31
+ const rel = relative(projectRoot, resolved);
32
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
33
+ }
34
+ function withinProject(p) {
35
+ const cwd = process.cwd();
36
+ return withinProjectPath(cwd, p) || relative(cwd, p) === ".";
37
+ }
38
+
39
+ // src/template-engine/index.ts
4
40
  var API_VERSION = "^0.1.10";
5
41
  var templates = /* @__PURE__ */ new Map();
42
+ var contributorUnregister = null;
6
43
  function expandTemplate(template, variables) {
7
44
  let result = template;
8
45
  result = result.replace(/\{\{(\w+)\}\}/g, (match, key) => {
@@ -47,9 +84,12 @@ function renderTemplateRaw(template, variables) {
47
84
  return result;
48
85
  }
49
86
  function validateRelativeTemplatePath(field, value) {
50
- if (isAbsolute(value) || value.split(/[\\/]+/).includes("..")) {
87
+ if (isAbsolute2(value) || value.split(/[\\/]+/).includes("..")) {
51
88
  return `${field} must be a relative path without ".." components`;
52
89
  }
90
+ if (!withinProject(value)) {
91
+ return `${field} must resolve inside the project directory`;
92
+ }
53
93
  return null;
54
94
  }
55
95
  var plugin = {
@@ -272,7 +312,8 @@ var plugin = {
272
312
  };
273
313
  }
274
314
  });
275
- api.registerSystemPromptContributor(async () => [
315
+ contributorUnregister = releaseHandle(contributorUnregister);
316
+ contributorUnregister = api.registerSystemPromptContributor(async () => [
276
317
  {
277
318
  type: "text",
278
319
  text: `Template engine available:
@@ -287,6 +328,7 @@ var plugin = {
287
328
  teardown(api) {
288
329
  const count = templates.size;
289
330
  templates.clear();
331
+ contributorUnregister = releaseHandle(contributorUnregister);
290
332
  api.log.info("template-engine: teardown complete", { cleared: count });
291
333
  },
292
334
  async health() {
@@ -3,6 +3,22 @@ import { readFileSync } from "node:fs";
3
3
 
4
4
  // src/runtime/index.ts
5
5
  import { basename, extname, isAbsolute, relative, resolve } from "node:path";
6
+
7
+ // src/runtime/local-bin.ts
8
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
9
+
10
+ // src/runtime/handles.ts
11
+ function releaseHandle(off) {
12
+ if (off) {
13
+ try {
14
+ off();
15
+ } catch {
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+
21
+ // src/runtime/index.ts
6
22
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
7
23
  function hasLeadingDash(arg) {
8
24
  return arg.length > 0 && arg.startsWith("-");
@@ -130,7 +146,7 @@ var plugin = {
130
146
  state.skippedCount = 0;
131
147
  state.lastOverallPct = null;
132
148
  state.lastResult = null;
133
- state.hookUnregister = null;
149
+ state.hookUnregister = releaseHandle(state.hookUnregister);
134
150
  const cfg = readConfig(api.config.extensions?.["test-coverage-gate"]);
135
151
  const hook = (input) => {
136
152
  if (!cfg.enabled) return;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-runner-gate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAscrD,QAAA,MAAM,MAAM,EAAE,MA+Tb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-runner-gate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAufrD,QAAA,MAAM,MAAM,EAAE,MA+Tb,CAAC;eAEa,MAAM"}
@@ -2,9 +2,120 @@
2
2
  import { execFile } from "node:child_process";
3
3
  import { access } from "node:fs/promises";
4
4
  import { basename as basename2, dirname, isAbsolute as isAbsolute2, join } from "node:path";
5
+ import { buildWin32CmdShimInvocation as buildWin32CmdShimInvocation2, resolveWin32Command as resolveWin32Command2 } from "@wrongstack/tools/win32";
5
6
 
6
7
  // src/runtime/index.ts
7
8
  import { basename, extname, isAbsolute, relative, resolve } from "node:path";
9
+
10
+ // src/runtime/local-bin.ts
11
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
12
+
13
+ // src/runtime/bounded-map.ts
14
+ var BoundedMap = class {
15
+ map = /* @__PURE__ */ new Map();
16
+ max;
17
+ ttlMs;
18
+ now;
19
+ /** Entries dropped to stay under `max`. Surfaced by plugin health(). */
20
+ evictions = 0;
21
+ constructor(options) {
22
+ const normalizedMax = Math.floor(options.max);
23
+ this.max = Number.isSafeInteger(normalizedMax) ? Math.max(1, normalizedMax) : 1;
24
+ this.ttlMs = options.ttlMs;
25
+ this.now = options.now ?? Date.now;
26
+ }
27
+ expired(entry) {
28
+ return this.ttlMs !== void 0 && this.now() - entry.storedAt > this.ttlMs;
29
+ }
30
+ get(key) {
31
+ const entry = this.map.get(key);
32
+ if (entry === void 0) return void 0;
33
+ if (this.expired(entry)) {
34
+ this.map.delete(key);
35
+ return void 0;
36
+ }
37
+ this.map.delete(key);
38
+ this.map.set(key, entry);
39
+ return entry.value;
40
+ }
41
+ /**
42
+ * Read without promoting the key to most-recently-used. Use for
43
+ * diagnostics that must not perturb the eviction order.
44
+ */
45
+ peek(key) {
46
+ const entry = this.map.get(key);
47
+ if (entry === void 0 || this.expired(entry)) return void 0;
48
+ return entry.value;
49
+ }
50
+ has(key) {
51
+ const entry = this.map.get(key);
52
+ if (entry === void 0) return false;
53
+ if (this.expired(entry)) {
54
+ this.map.delete(key);
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+ set(key, value) {
60
+ this.map.delete(key);
61
+ this.map.set(key, { value, storedAt: this.now() });
62
+ while (this.map.size > this.max) {
63
+ const coldest = this.map.keys().next().value;
64
+ if (coldest === void 0) break;
65
+ this.map.delete(coldest);
66
+ this.evictions += 1;
67
+ }
68
+ return this;
69
+ }
70
+ delete(key) {
71
+ return this.map.delete(key);
72
+ }
73
+ clear() {
74
+ this.map.clear();
75
+ this.evictions = 0;
76
+ }
77
+ get size() {
78
+ return this.map.size;
79
+ }
80
+ /** How many entries have been dropped to respect `max`, since the last clear. */
81
+ get evictionCount() {
82
+ return this.evictions;
83
+ }
84
+ /** Drop every expired entry. Cheap enough to call from a status tool. */
85
+ prune() {
86
+ if (this.ttlMs === void 0) return 0;
87
+ let removed = 0;
88
+ for (const [key, entry] of this.map) {
89
+ if (this.expired(entry)) {
90
+ this.map.delete(key);
91
+ removed += 1;
92
+ }
93
+ }
94
+ return removed;
95
+ }
96
+ /** Live (non-expired) entries, coldest first. */
97
+ *entries() {
98
+ for (const [key, entry] of this.map) {
99
+ if (!this.expired(entry)) yield [key, entry.value];
100
+ }
101
+ }
102
+ [Symbol.iterator]() {
103
+ return this.entries();
104
+ }
105
+ };
106
+
107
+ // src/runtime/handles.ts
108
+ function releaseHandle(off) {
109
+ if (off) {
110
+ try {
111
+ off();
112
+ } catch {
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+
118
+ // src/runtime/index.ts
8
119
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
9
120
  function hasLeadingDash(arg) {
10
121
  return arg.length > 0 && arg.startsWith("-");
@@ -22,6 +133,15 @@ function withinProject(p) {
22
133
  }
23
134
 
24
135
  // src/test-runner-gate/index.ts
136
+ function resolveExec(command, args) {
137
+ const resolved = resolveWin32Command2(command);
138
+ const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
139
+ if (needsShell) {
140
+ const shim = buildWin32CmdShimInvocation2(resolved, args);
141
+ return { cmd: shim.command, args: shim.args, windowsVerbatimArguments: true };
142
+ }
143
+ return { cmd: resolved, args: [...args], windowsVerbatimArguments: false };
144
+ }
25
145
  var API_VERSION = "^0.1.10";
26
146
  var state = {
27
147
  invocationCount: 0,
@@ -86,7 +206,7 @@ function pathContentHash(content) {
86
206
  }
87
207
  return h >>> 0;
88
208
  }
89
- var lastPassedHash = /* @__PURE__ */ new Map();
209
+ var lastPassedHash = new BoundedMap({ max: 1024 });
90
210
  var ALLOWED_COMMAND_TOKENS = /* @__PURE__ */ new Set([
91
211
  "npx",
92
212
  "pnpm",
@@ -141,10 +261,13 @@ async function detectRunner(requested) {
141
261
  if (!match) return null;
142
262
  try {
143
263
  await new Promise((resolve2, reject) => {
144
- execFile("npx", [`${match.name}`, "--version"], {
264
+ const ex = resolveExec("npx", [`${match.name}`, "--version"]);
265
+ execFile(ex.cmd, ex.args, {
145
266
  encoding: "utf-8",
146
267
  timeout: 5e3,
147
- cwd: process.cwd()
268
+ cwd: process.cwd(),
269
+ windowsHide: true,
270
+ ...ex.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
148
271
  }, (err) => err ? reject(err) : resolve2());
149
272
  });
150
273
  return match;
@@ -155,10 +278,13 @@ async function detectRunner(requested) {
155
278
  for (const candidate of candidates) {
156
279
  try {
157
280
  await new Promise((resolve2, reject) => {
158
- execFile("npx", [`${candidate.name}`, "--version"], {
281
+ const ex = resolveExec("npx", [`${candidate.name}`, "--version"]);
282
+ execFile(ex.cmd, ex.args, {
159
283
  encoding: "utf-8",
160
284
  timeout: 5e3,
161
- cwd: process.cwd()
285
+ cwd: process.cwd(),
286
+ windowsHide: true,
287
+ ...ex.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
162
288
  }, (err) => err ? reject(err) : resolve2());
163
289
  });
164
290
  return candidate;
@@ -206,10 +332,17 @@ async function runTests(testFile, runner, customCommand, timeoutMs) {
206
332
  try {
207
333
  const { stdout: out } = await new Promise(
208
334
  (resolve2, reject) => {
335
+ const ex = resolveExec(cmd, fullArgs);
209
336
  execFile(
210
- cmd,
211
- fullArgs,
212
- { encoding: "utf-8", timeout: timeoutMs, cwd: process.cwd() },
337
+ ex.cmd,
338
+ ex.args,
339
+ {
340
+ encoding: "utf-8",
341
+ timeout: timeoutMs,
342
+ cwd: process.cwd(),
343
+ windowsHide: true,
344
+ ...ex.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
345
+ },
213
346
  (err, out2, stderr) => {
214
347
  if (err) reject(Object.assign(err, { stdout: out2, stderr }));
215
348
  else resolve2({ stdout: out2, stderr });
@@ -329,7 +462,7 @@ var plugin = {
329
462
  state.errorCount = 0;
330
463
  state.extensionSkippedCount = 0;
331
464
  state.cachedSkipCount = 0;
332
- state.hookUnregister = null;
465
+ state.hookUnregister = releaseHandle(state.hookUnregister);
333
466
  state.lastResult = null;
334
467
  lastPassedHash.clear();
335
468
  const cfg = readConfig(api.config.extensions?.["test-runner-gate"]);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/todo-listener/index.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAmGrD,QAAA,MAAM,MAAM,EAAE,MAqNb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/todo-listener/index.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAoGrD,QAAA,MAAM,MAAM,EAAE,MAuNb,CAAC;eAEa,MAAM"}
@@ -1,3 +1,43 @@
1
+ // src/runtime/local-bin.ts
2
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
3
+
4
+ // src/runtime/safe-json.ts
5
+ var UNSERIALIZABLE = "[unserializable]";
6
+ function safeJsonStringify(value, indent) {
7
+ try {
8
+ const stack = [];
9
+ const out = JSON.stringify(
10
+ value,
11
+ function replacer(_key, val) {
12
+ if (typeof val === "bigint") return `${val.toString()}n`;
13
+ if (val === null || typeof val !== "object") return val;
14
+ while (stack.length > 0 && stack[stack.length - 1] !== this) stack.pop();
15
+ if (stack.includes(val)) return "[circular]";
16
+ stack.push(val);
17
+ return val;
18
+ },
19
+ indent
20
+ );
21
+ return out ?? String(value);
22
+ } catch {
23
+ return UNSERIALIZABLE;
24
+ }
25
+ }
26
+
27
+ // src/runtime/handles.ts
28
+ function releaseHandle(off) {
29
+ if (off) {
30
+ try {
31
+ off();
32
+ } catch {
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+
38
+ // src/runtime/index.ts
39
+ var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
40
+
1
41
  // src/todo-listener/index.ts
2
42
  var state = {
3
43
  invocationCount: 0,
@@ -71,7 +111,7 @@ var plugin = {
71
111
  state.lastMessageId = null;
72
112
  state.lastPayloadHash = "";
73
113
  state.lastBroadcastAt = 0;
74
- state.hookUnregister = null;
114
+ state.hookUnregister = releaseHandle(state.hookUnregister);
75
115
  const cfg = readConfig(api.config.extensions?.["todo-listener"]);
76
116
  const mailbox = api.mailbox;
77
117
  const hook = async (input) => {
@@ -112,7 +152,7 @@ var plugin = {
112
152
  0,
113
153
  200
114
154
  );
115
- const body = JSON.stringify(payload, null, 2);
155
+ const body = safeJsonStringify(payload, 2);
116
156
  const sendInput = {
117
157
  from: `plugin:todo-listener`,
118
158
  to: "*",
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/todo-tracker/index.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAmKrD,QAAA,MAAM,MAAM,EAAE,MAsZb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/todo-tracker/index.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAuKrD,QAAA,MAAM,MAAM,EAAE,MAsZb,CAAC;eAEa,MAAM"}
@@ -1,6 +1,7 @@
1
1
  // src/todo-tracker/index.ts
2
2
  import * as fsp from "node:fs/promises";
3
3
  import { randomUUID } from "node:crypto";
4
+ import { atomicWrite, ensureDir } from "@wrongstack/core/utils";
4
5
  function deriveFilePath(api) {
5
6
  const raw = api.config.extensions?.["todo-tracker"];
6
7
  const explicit = typeof raw?.["filePath"] === "string" ? raw["filePath"] : null;
@@ -30,10 +31,8 @@ async function loadFile(filePath) {
30
31
  }
31
32
  }
32
33
  async function saveFile(filePath, file) {
33
- const tmp = `${filePath}.${randomUUID().slice(0, 8)}.tmp`;
34
- await fsp.mkdir(filePath.replace(/[/\\][^/\\]+$/, ""), { recursive: true });
35
- await fsp.writeFile(tmp, JSON.stringify(file, null, 2), { encoding: "utf8", mode: 384 });
36
- await fsp.rename(tmp, filePath);
34
+ await ensureDir(filePath.replace(/[/\\][^/\\]+$/, ""));
35
+ await atomicWrite(filePath, JSON.stringify(file, null, 2), { mode: 384 });
37
36
  }
38
37
  var state = {
39
38
  filePath: null,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/token-budget/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAqFrD,QAAA,MAAM,MAAM,EAAE,MA8Qb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/token-budget/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AA8HrD,QAAA,MAAM,MAAM,EAAE,MAyQb,CAAC;eAEa,MAAM"}
@@ -34,16 +34,36 @@ function modelMatches(pattern, model) {
34
34
  }
35
35
  return model.toLowerCase() === pattern.toLowerCase();
36
36
  }
37
+ function readBoundedNumber(value, min, max, fallback) {
38
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
39
+ if (value < min || value > max) return fallback;
40
+ return value;
41
+ }
37
42
  function readConfig(raw) {
38
43
  if (!raw || typeof raw !== "object") return { ...DEFAULTS };
39
44
  const r = raw;
45
+ const warnPercent = readBoundedNumber(r["warnPercent"], 1, 100, DEFAULTS.warnPercent);
46
+ const stopPercent = readBoundedNumber(r["stopPercent"], 1, 100, DEFAULTS.stopPercent);
40
47
  return {
41
- limit: typeof r["limit"] === "number" ? r["limit"] : DEFAULTS.limit,
42
- warnPercent: typeof r["warnPercent"] === "number" ? r["warnPercent"] : DEFAULTS.warnPercent,
43
- stopPercent: typeof r["stopPercent"] === "number" ? r["stopPercent"] : DEFAULTS.stopPercent,
48
+ limit: readBoundedNumber(r["limit"], 0, Number.MAX_SAFE_INTEGER, DEFAULTS.limit),
49
+ warnPercent,
50
+ // A warn threshold above the stop threshold can never fire — the run
51
+ // stops first. Clamp so the pair always describes a reachable window.
52
+ stopPercent: Math.max(stopPercent, warnPercent),
44
53
  model: typeof r["model"] === "string" ? r["model"] : ""
45
54
  };
46
55
  }
56
+ function clearRegistrations() {
57
+ for (const key of ["hookUnregister", "postHookUnregister"]) {
58
+ const off = state[key];
59
+ if (!off) continue;
60
+ try {
61
+ off();
62
+ } catch {
63
+ }
64
+ state[key] = null;
65
+ }
66
+ }
47
67
  var plugin = {
48
68
  name: "token-budget",
49
69
  version: "0.1.0",
@@ -90,9 +110,8 @@ var plugin = {
90
110
  state.stopFired = false;
91
111
  state.warnContextInjected = false;
92
112
  state.stopContextInjected = false;
93
- state.hookUnregister = null;
94
- state.postHookUnregister = null;
95
113
  state.lastRequest = null;
114
+ clearRegistrations();
96
115
  const cfg = readConfig(api.config.extensions?.["token-budget"]);
97
116
  api.onEvent("provider.response", (payload) => {
98
117
  const p = payload;
@@ -189,6 +208,12 @@ var plugin = {
189
208
  remaining,
190
209
  percent,
191
210
  requestCount: state.requestCount,
211
+ // The EFFECTIVE thresholds, after out-of-range values fall back to
212
+ // the defaults and warn/stop are ordered. Without these the user
213
+ // cannot tell that a rejected or clamped setting is not in force.
214
+ warnPercent: cfg.warnPercent,
215
+ stopPercent: cfg.stopPercent,
216
+ model: cfg.model === "" ? null : cfg.model,
192
217
  breakdown: {
193
218
  prompt: state.totalPromptTokens,
194
219
  completion: state.totalCompletionTokens
@@ -207,20 +232,7 @@ var plugin = {
207
232
  });
208
233
  },
209
234
  teardown(api) {
210
- if (state.hookUnregister) {
211
- try {
212
- state.hookUnregister();
213
- } catch {
214
- }
215
- state.hookUnregister = null;
216
- }
217
- if (state.postHookUnregister) {
218
- try {
219
- state.postHookUnregister();
220
- } catch {
221
- }
222
- state.postHookUnregister = null;
223
- }
235
+ clearRegistrations();
224
236
  const final = {
225
237
  totalTokens: state.totalTokens,
226
238
  requestCount: state.requestCount,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/token-throttle/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,MAAM,EAAa,MAAM,wBAAwB,CAAC;AA8ChE,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,CAE5E;AAED,0CAA0C;AAC1C,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,UAAU,EAAE,EACrB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,CAoBR;AA4CD,QAAA,MAAM,MAAM,EAAE,MAoKb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/token-throttle/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,MAAM,EAAa,MAAM,wBAAwB,CAAC;AA8ChE,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,CAE5E;AAED,0CAA0C;AAC1C,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,UAAU,EAAE,EACrB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,CAoBR;AAyED,QAAA,MAAM,MAAM,EAAE,MAqKb,CAAC;eAEa,MAAM"}
@@ -60,7 +60,21 @@ function estimateRequestTokens(request, charsPerToken) {
60
60
  walk(request["messages"]);
61
61
  return Math.ceil(chars / charsPerToken);
62
62
  }
63
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
63
+ function sleep(ms, signal) {
64
+ if (signal?.aborted) return Promise.resolve();
65
+ return new Promise((resolve) => {
66
+ const timer = setTimeout(() => {
67
+ signal?.removeEventListener("abort", onAbort);
68
+ resolve();
69
+ }, ms);
70
+ timer.unref?.();
71
+ function onAbort() {
72
+ clearTimeout(timer);
73
+ resolve();
74
+ }
75
+ signal?.addEventListener("abort", onAbort, { once: true });
76
+ });
77
+ }
64
78
  var plugin = {
65
79
  name: "token-throttle",
66
80
  version: "0.1.0",
@@ -119,6 +133,7 @@ var plugin = {
119
133
  name: "token-throttle",
120
134
  owner: "token-throttle",
121
135
  async wrapProviderRunner(_ctx, request, inner) {
136
+ const signal = _ctx?.signal;
122
137
  const req = request ?? {};
123
138
  state.invocations += 1;
124
139
  const now = Date.now();
@@ -132,7 +147,7 @@ var plugin = {
132
147
  api.metrics.counter("throttled");
133
148
  api.metrics.histogram("delay_ms", delay);
134
149
  api.log.info("token-throttle: delaying provider call", { delayMs: delay, projected });
135
- await sleep(delay);
150
+ await sleep(delay, signal);
136
151
  }
137
152
  const response = await inner(_ctx, request);
138
153
  const used = (response?.usage?.input ?? 0) + (response?.usage?.output ?? 0) || projected;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/type-gate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAuNrD,QAAA,MAAM,MAAM,EAAE,MAkOb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/type-gate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAgOrD,QAAA,MAAM,MAAM,EAAE,MAkOb,CAAC;eAEa,MAAM"}