@wrongstack/plugins 0.295.0 → 0.295.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.
Files changed (90) hide show
  1. package/dist/accessibility-auditor/index.d.ts.map +1 -1
  2. package/dist/accessibility-auditor.js +19 -2
  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 +103 -1
  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 +17 -7
  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.js +5 -0
  25. package/dist/dependency-vulnerability-gate/index.d.ts.map +1 -1
  26. package/dist/dependency-vulnerability-gate.js +43 -8
  27. package/dist/diff-summary/index.d.ts.map +1 -1
  28. package/dist/diff-summary.js +100 -1
  29. package/dist/doc-sync-guard.js +5 -0
  30. package/dist/duplicate-code-detector/index.d.ts.map +1 -1
  31. package/dist/duplicate-code-detector.js +102 -2
  32. package/dist/feature-flag-tracker/index.d.ts.map +1 -1
  33. package/dist/feature-flag-tracker.js +30 -4
  34. package/dist/file-watcher/index.d.ts.map +1 -1
  35. package/dist/file-watcher.js +6 -7
  36. package/dist/format-on-save/index.d.ts +2 -0
  37. package/dist/format-on-save/index.d.ts.map +1 -1
  38. package/dist/format-on-save.js +288 -59
  39. package/dist/import-organizer/index.d.ts.map +1 -1
  40. package/dist/import-organizer.js +140 -20
  41. package/dist/index.js +1265 -544
  42. package/dist/interface-contract-guard/index.d.ts.map +1 -1
  43. package/dist/interface-contract-guard.js +60 -26
  44. package/dist/knowledge-graph/index.d.ts.map +1 -1
  45. package/dist/knowledge-graph.js +7 -6
  46. package/dist/lint-gate/index.d.ts.map +1 -1
  47. package/dist/lint-gate.js +103 -1
  48. package/dist/loop-breaker/index.d.ts.map +1 -1
  49. package/dist/loop-breaker.js +15 -5
  50. package/dist/migration-planner.js +3 -0
  51. package/dist/notify-hub/index.d.ts.map +1 -1
  52. package/dist/notify-hub.js +41 -4
  53. package/dist/prompt-firewall/index.d.ts.map +1 -1
  54. package/dist/prompt-firewall.js +140 -11
  55. package/dist/refactor-suggester/index.d.ts.map +1 -1
  56. package/dist/refactor-suggester.js +119 -4
  57. package/dist/runtime/bounded-map.d.ts +86 -0
  58. package/dist/runtime/bounded-map.d.ts.map +1 -0
  59. package/dist/runtime/credential-patterns.d.ts +42 -0
  60. package/dist/runtime/credential-patterns.d.ts.map +1 -0
  61. package/dist/runtime/index.d.ts +3 -0
  62. package/dist/runtime/index.d.ts.map +1 -1
  63. package/dist/runtime/local-bin.d.ts +120 -0
  64. package/dist/runtime/local-bin.d.ts.map +1 -0
  65. package/dist/runtime/safe-json.d.ts +25 -0
  66. package/dist/runtime/safe-json.d.ts.map +1 -0
  67. package/dist/runtime.js +291 -22
  68. package/dist/schema-evolution-guard.js +5 -0
  69. package/dist/secret-scanner/index.d.ts.map +1 -1
  70. package/dist/secret-scanner.js +175 -48
  71. package/dist/security-hotspot-scanner/index.d.ts.map +1 -1
  72. package/dist/security-hotspot-scanner.js +132 -1
  73. package/dist/semantic-search-indexer/index.d.ts.map +1 -1
  74. package/dist/semantic-search-indexer.js +7 -1
  75. package/dist/semver-bump/index.d.ts.map +1 -1
  76. package/dist/semver-bump.js +21 -2
  77. package/dist/template-engine/index.d.ts.map +1 -1
  78. package/dist/template-engine.js +30 -2
  79. package/dist/test-coverage-gate.js +5 -0
  80. package/dist/test-runner-gate/index.d.ts.map +1 -1
  81. package/dist/test-runner-gate.js +130 -8
  82. package/dist/todo-listener/index.d.ts.map +1 -1
  83. package/dist/todo-listener.js +30 -1
  84. package/dist/todo-tracker/index.d.ts.map +1 -1
  85. package/dist/todo-tracker.js +3 -4
  86. package/dist/token-throttle/index.d.ts.map +1 -1
  87. package/dist/token-throttle.js +17 -2
  88. package/dist/type-gate/index.d.ts.map +1 -1
  89. package/dist/type-gate.js +96 -16
  90. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/interface-contract-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AASrD,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAwID,QAAA,MAAM,MAAM,EAAE,MAuMb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/interface-contract-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AASrD,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAqLD,QAAA,MAAM,MAAM,EAAE,MAyNb,CAAC;eAEa,MAAM"}
@@ -4,6 +4,11 @@ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 }
4
4
 
5
5
  // src/runtime/index.ts
6
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/index.ts
7
12
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
8
13
  function hasLeadingDash(arg) {
9
14
  return arg.length > 0 && arg.startsWith("-");
@@ -74,7 +79,8 @@ var state = {
74
79
  var DEFAULTS = {
75
80
  enabled: false,
76
81
  extensions: [".ts", ".tsx"],
77
- maxFindings: 50
82
+ maxFindings: 50,
83
+ maxFiles: 2e3
78
84
  };
79
85
  function readConfig(raw) {
80
86
  if (!raw || typeof raw !== "object") return { ...DEFAULTS };
@@ -82,7 +88,8 @@ function readConfig(raw) {
82
88
  return {
83
89
  enabled: r["enabled"] !== false,
84
90
  extensions: Array.isArray(r["extensions"]) ? r["extensions"].filter((x) => typeof x === "string") : DEFAULTS.extensions,
85
- maxFindings: typeof r["maxFindings"] === "number" && r["maxFindings"] >= 1 && r["maxFindings"] <= 500 ? r["maxFindings"] : DEFAULTS.maxFindings
91
+ maxFindings: typeof r["maxFindings"] === "number" && r["maxFindings"] >= 1 && r["maxFindings"] <= 500 ? r["maxFindings"] : DEFAULTS.maxFindings,
92
+ maxFiles: typeof r["maxFiles"] === "number" && r["maxFiles"] >= 1 && r["maxFiles"] <= 5e4 ? Math.floor(r["maxFiles"]) : DEFAULTS.maxFiles
86
93
  };
87
94
  }
88
95
  function normalizeExtensions(exts) {
@@ -98,47 +105,60 @@ var INTERFACE_RE = /(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
98
105
  function extractInterfaceNames(content) {
99
106
  const names = [];
100
107
  INTERFACE_RE.lastIndex = 0;
108
+ let searchedUpTo = 0;
109
+ let line = 1;
101
110
  for (const m of content.matchAll(INTERFACE_RE)) {
102
- names.push({ name: m[1], line: content.slice(0, m.index).split(/\r?\n/).length });
111
+ const index = m.index ?? 0;
112
+ for (let i = searchedUpTo; i < index; i++) {
113
+ if (content.charCodeAt(i) === 10) line += 1;
114
+ }
115
+ searchedUpTo = index;
116
+ names.push({ name: m[1], line });
103
117
  }
104
118
  return names;
105
119
  }
106
- function hasImplementer(name, contents) {
107
- const implRe = new RegExp(`(?:implements|satisfies|\\bas)\\s+${name}\\b`, "g");
108
- for (const content of contents) {
109
- if (implRe.test(content)) return true;
120
+ var IMPLEMENTER_RE = /(?:implements|satisfies|\bas)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
121
+ function collectImplementedNames(content, into) {
122
+ IMPLEMENTER_RE.lastIndex = 0;
123
+ for (const m of content.matchAll(IMPLEMENTER_RE)) {
124
+ const name = m[1];
125
+ if (name) into.add(name);
110
126
  }
111
- return false;
112
127
  }
113
128
  async function scanPath(rawPath, cfg) {
114
129
  const root = process.cwd();
115
130
  const resolved = isAbsolute2(rawPath) ? resolve2(rawPath) : resolve2(root, rawPath);
116
131
  const exts = normalizeExtensions(cfg.extensions);
117
- const files = await collectSourceFilesAsync(resolved, { extensions: exts });
118
- const contents = [];
132
+ const allFiles = await collectSourceFilesAsync(resolved, { extensions: exts });
133
+ const files = allFiles.slice(0, cfg.maxFiles);
134
+ const implemented = /* @__PURE__ */ new Set();
135
+ const declarations = [];
136
+ let scannedFiles = 0;
119
137
  for (const p of files) {
138
+ let content;
120
139
  try {
121
- contents.push({ path: p, content: await readFile(p, "utf-8") });
140
+ content = await readFile(p, "utf-8");
122
141
  } catch {
142
+ continue;
123
143
  }
124
- }
125
- const allContentStrings = contents.map((c) => c.content);
126
- const findings = [];
127
- for (const { path, content } of contents) {
144
+ scannedFiles += 1;
145
+ collectImplementedNames(content, implemented);
128
146
  for (const { name, line } of extractInterfaceNames(content)) {
129
- if (!hasImplementer(name, allContentStrings)) {
130
- findings.push({
131
- interfaceName: name,
132
- file: relativePath(path),
133
- line,
134
- message: `interface "${name}" is declared but has no visible implementer`
135
- });
136
- if (findings.length >= cfg.maxFindings) break;
137
- }
147
+ declarations.push({ name, line, file: p });
138
148
  }
149
+ }
150
+ const findings = [];
151
+ for (const { name, line, file } of declarations) {
152
+ if (implemented.has(name)) continue;
153
+ findings.push({
154
+ interfaceName: name,
155
+ file: relativePath(file),
156
+ line,
157
+ message: `interface "${name}" is declared but has no visible implementer`
158
+ });
139
159
  if (findings.length >= cfg.maxFindings) break;
140
160
  }
141
- return { findings, scannedFiles: contents.length };
161
+ return { findings, scannedFiles, truncated: allFiles.length > files.length };
142
162
  }
143
163
  var plugin = {
144
164
  name: "interface-contract-guard",
@@ -157,6 +177,13 @@ var plugin = {
157
177
  default: [".ts", ".tsx"],
158
178
  description: "File extensions to scan."
159
179
  },
180
+ maxFiles: {
181
+ type: "number",
182
+ minimum: 1,
183
+ maximum: 5e4,
184
+ default: 2e3,
185
+ description: "Upper bound on files read per scan."
186
+ },
160
187
  maxFindings: {
161
188
  type: "number",
162
189
  minimum: 1,
@@ -240,7 +267,14 @@ If you changed member shapes, search the project for implementers/\`satisfies\`/
240
267
  ok: true,
241
268
  path: relativePath(resolve2(process.cwd(), rawPath)),
242
269
  scannedFiles: result.scannedFiles,
243
- findings: result.findings
270
+ findings: result.findings,
271
+ // Say so when the corpus was cut short. A partial scan that
272
+ // reports "no findings" is not the same as a clean result, and
273
+ // the caller has no other way to tell the difference.
274
+ truncated: result.truncated,
275
+ ...result.truncated ? {
276
+ note: `Scan stopped at the maxFiles limit (${cfg.maxFiles}); results are partial. Raise \`maxFiles\` or scan a narrower path for full coverage.`
277
+ } : {}
244
278
  };
245
279
  }
246
280
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/knowledge-graph/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAQrD,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;CACnB;AA2HD,QAAA,MAAM,MAAM,EAAE,MA2Sb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/knowledge-graph/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AASrD,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;CACnB;AAiID,QAAA,MAAM,MAAM,EAAE,MA2Sb,CAAC;eAEa,MAAM"}
@@ -1,6 +1,7 @@
1
1
  // src/knowledge-graph/index.ts
2
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { readFileSync } from "node:fs";
3
3
  import { dirname, isAbsolute, relative, resolve } from "node:path";
4
+ import { atomicWrite, ensureDir } from "@wrongstack/core/utils";
4
5
  var API_VERSION = "^0.1.10";
5
6
  var state = {
6
7
  facts: [],
@@ -52,11 +53,11 @@ function loadFacts(filePath) {
52
53
  return { facts: [], nextId: 1 };
53
54
  }
54
55
  }
55
- function persistFacts(filePath) {
56
+ async function persistFacts(filePath) {
56
57
  if (!filePath) return true;
57
58
  try {
58
- mkdirSync(dirname(filePath), { recursive: true });
59
- writeFileSync(
59
+ await ensureDir(dirname(filePath));
60
+ await atomicWrite(
60
61
  filePath,
61
62
  JSON.stringify({ facts: state.facts, nextId: state.nextId }, null, 2)
62
63
  );
@@ -187,7 +188,7 @@ var plugin = {
187
188
  state.facts.push(fact);
188
189
  state.adds += 1;
189
190
  api.metrics.counter("adds");
190
- const persisted = persistFacts(resolved);
191
+ const persisted = await persistFacts(resolved);
191
192
  return { ok: true, fact, persisted, totalFacts: state.facts.length };
192
193
  }
193
194
  });
@@ -247,7 +248,7 @@ var plugin = {
247
248
  if (removed === 0) return { ok: false, error: `no fact matches "${input.id}"` };
248
249
  state.removals += removed;
249
250
  api.metrics.counter("removals", removed);
250
- const persisted = persistFacts(resolved);
251
+ const persisted = await persistFacts(resolved);
251
252
  return { ok: true, removed, persisted, totalFacts: state.facts.length };
252
253
  }
253
254
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lint-gate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAQH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AA8VrD,QAAA,MAAM,MAAM,EAAE,MA2Wb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lint-gate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAQH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AA+VrD,QAAA,MAAM,MAAM,EAAE,MA2Wb,CAAC;eAEa,MAAM"}
package/dist/lint-gate.js CHANGED
@@ -5,6 +5,108 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
5
  import { createRequire } from "node:module";
6
6
  import { tmpdir } from "node:os";
7
7
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
+
9
+ // src/runtime/local-bin.ts
10
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
11
+
12
+ // src/runtime/bounded-map.ts
13
+ var BoundedMap = class {
14
+ map = /* @__PURE__ */ new Map();
15
+ max;
16
+ ttlMs;
17
+ now;
18
+ /** Entries dropped to stay under `max`. Surfaced by plugin health(). */
19
+ evictions = 0;
20
+ constructor(options) {
21
+ const normalizedMax = Math.floor(options.max);
22
+ this.max = Number.isSafeInteger(normalizedMax) ? Math.max(1, normalizedMax) : 1;
23
+ this.ttlMs = options.ttlMs;
24
+ this.now = options.now ?? Date.now;
25
+ }
26
+ expired(entry) {
27
+ return this.ttlMs !== void 0 && this.now() - entry.storedAt > this.ttlMs;
28
+ }
29
+ get(key) {
30
+ const entry = this.map.get(key);
31
+ if (entry === void 0) return void 0;
32
+ if (this.expired(entry)) {
33
+ this.map.delete(key);
34
+ return void 0;
35
+ }
36
+ this.map.delete(key);
37
+ this.map.set(key, entry);
38
+ return entry.value;
39
+ }
40
+ /**
41
+ * Read without promoting the key to most-recently-used. Use for
42
+ * diagnostics that must not perturb the eviction order.
43
+ */
44
+ peek(key) {
45
+ const entry = this.map.get(key);
46
+ if (entry === void 0 || this.expired(entry)) return void 0;
47
+ return entry.value;
48
+ }
49
+ has(key) {
50
+ const entry = this.map.get(key);
51
+ if (entry === void 0) return false;
52
+ if (this.expired(entry)) {
53
+ this.map.delete(key);
54
+ return false;
55
+ }
56
+ return true;
57
+ }
58
+ set(key, value) {
59
+ this.map.delete(key);
60
+ this.map.set(key, { value, storedAt: this.now() });
61
+ while (this.map.size > this.max) {
62
+ const coldest = this.map.keys().next().value;
63
+ if (coldest === void 0) break;
64
+ this.map.delete(coldest);
65
+ this.evictions += 1;
66
+ }
67
+ return this;
68
+ }
69
+ delete(key) {
70
+ return this.map.delete(key);
71
+ }
72
+ clear() {
73
+ this.map.clear();
74
+ this.evictions = 0;
75
+ }
76
+ get size() {
77
+ return this.map.size;
78
+ }
79
+ /** How many entries have been dropped to respect `max`, since the last clear. */
80
+ get evictionCount() {
81
+ return this.evictions;
82
+ }
83
+ /** Drop every expired entry. Cheap enough to call from a status tool. */
84
+ prune() {
85
+ if (this.ttlMs === void 0) return 0;
86
+ let removed = 0;
87
+ for (const [key, entry] of this.map) {
88
+ if (this.expired(entry)) {
89
+ this.map.delete(key);
90
+ removed += 1;
91
+ }
92
+ }
93
+ return removed;
94
+ }
95
+ /** Live (non-expired) entries, coldest first. */
96
+ *entries() {
97
+ for (const [key, entry] of this.map) {
98
+ if (!this.expired(entry)) yield [key, entry.value];
99
+ }
100
+ }
101
+ [Symbol.iterator]() {
102
+ return this.entries();
103
+ }
104
+ };
105
+
106
+ // src/runtime/index.ts
107
+ var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
108
+
109
+ // src/lint-gate/index.ts
8
110
  var API_VERSION = "^0.1.10";
9
111
  var state = {
10
112
  /** Total PreToolUse invocations. */
@@ -42,7 +144,7 @@ var LINTER_PACKAGES = {
42
144
  biome: "@biomejs/biome",
43
145
  eslint: "eslint"
44
146
  };
45
- var linterCache = /* @__PURE__ */ new Map();
147
+ var linterCache = new BoundedMap({ max: 32, ttlMs: 3e5 });
46
148
  function isInside(parent, candidate) {
47
149
  const rel = relative(parent, candidate);
48
150
  return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/loop-breaker/index.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAqQrD,QAAA,MAAM,MAAM,EAAE,MA+Yb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/loop-breaker/index.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAmSrD,QAAA,MAAM,MAAM,EAAE,MA+Yb,CAAC;eAEa,MAAM"}
@@ -66,16 +66,26 @@ function canonicalize(value) {
66
66
  return String(value);
67
67
  }
68
68
  }
69
- function sortKeys(value) {
70
- if (Array.isArray(value)) return value.map(sortKeys);
71
- if (value && typeof value === "object") {
69
+ var CANONICALIZE_MAX_DEPTH = 12;
70
+ function sortKeys(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
71
+ if (value === null || typeof value !== "object") return value;
72
+ if (seen.has(value)) return "[circular]";
73
+ if (depth >= CANONICALIZE_MAX_DEPTH) {
74
+ return Array.isArray(value) ? `[array:${value.length}]` : "[deep-object]";
75
+ }
76
+ seen.add(value);
77
+ try {
78
+ if (Array.isArray(value)) {
79
+ return value.map((v) => sortKeys(v, depth + 1, seen));
80
+ }
72
81
  const out = {};
73
82
  for (const key of Object.keys(value).sort()) {
74
- out[key] = sortKeys(value[key]);
83
+ out[key] = sortKeys(value[key], depth + 1, seen);
75
84
  }
76
85
  return out;
86
+ } finally {
87
+ seen.delete(value);
77
88
  }
78
- return value;
79
89
  }
80
90
  function fingerprint(toolName, toolInput) {
81
91
  return `${toolName}\0${canonicalize(toolInput)}`;
@@ -50,6 +50,9 @@ async function runOptionalPluginLlm(request) {
50
50
  }
51
51
  }
52
52
 
53
+ // src/runtime/local-bin.ts
54
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
55
+
53
56
  // src/runtime/index.ts
54
57
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
55
58
  function hasLeadingDash(arg) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notify-hub/index.ts"],"names":[],"mappings":"AAyDA,OAAO,KAAK,EAAU,MAAM,EAAE,MAAM,wBAAwB,CAAC;AA8N7D,QAAA,MAAM,MAAM,EAAE,MAwRb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notify-hub/index.ts"],"names":[],"mappings":"AAyDA,OAAO,KAAK,EAAU,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAiP7D,QAAA,MAAM,MAAM,EAAE,MAwRb,CAAC;eAEa,MAAM"}
@@ -1,6 +1,35 @@
1
1
  // src/notify-hub/index.ts
2
2
  import { lookup } from "node:dns/promises";
3
3
 
4
+ // src/runtime/local-bin.ts
5
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
6
+
7
+ // src/runtime/safe-json.ts
8
+ var UNSERIALIZABLE = "[unserializable]";
9
+ function safeJsonStringify(value, indent) {
10
+ try {
11
+ const stack = [];
12
+ const out = JSON.stringify(
13
+ value,
14
+ function replacer(_key, val) {
15
+ if (typeof val === "bigint") return `${val.toString()}n`;
16
+ if (val === null || typeof val !== "object") return val;
17
+ while (stack.length > 0 && stack[stack.length - 1] !== this) stack.pop();
18
+ if (stack.includes(val)) return "[circular]";
19
+ stack.push(val);
20
+ return val;
21
+ },
22
+ indent
23
+ );
24
+ return out ?? String(value);
25
+ } catch {
26
+ return UNSERIALIZABLE;
27
+ }
28
+ }
29
+
30
+ // src/runtime/index.ts
31
+ var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
32
+
4
33
  // src/notify-hub/webhook-channel.ts
5
34
  function freshCircuit() {
6
35
  return { open: false, consecutiveFailures: 0 };
@@ -202,13 +231,21 @@ async function deliver(event, payload) {
202
231
  }
203
232
  const result = await deliverViaChannel(ch, event, {
204
233
  title: typeof payload.title === "string" ? payload.title : event,
205
- body: typeof payload.message === "string" ? payload.message : typeof payload.error === "string" ? payload.error : JSON.stringify(payload),
234
+ body: typeof payload.message === "string" ? payload.message : typeof payload.error === "string" ? payload.error : safeJsonStringify(payload),
206
235
  level: event === "tool.error" || event === "budget.threshold" ? "warning" : "info",
207
236
  source: event,
208
237
  metadata: payload
209
238
  });
210
239
  return result.ok;
211
240
  }
241
+ function deliverDetached(event, payload) {
242
+ deliver(event, payload).catch((err) => {
243
+ _pluginLog?.warn("notify-hub: notification delivery failed", {
244
+ event,
245
+ error: err instanceof Error ? err.message : String(err)
246
+ });
247
+ });
248
+ }
212
249
  async function deliverViaChannel(ch, event, msg) {
213
250
  const result = await ch.deliver(msg);
214
251
  if (result.ok) {
@@ -317,7 +354,7 @@ var plugin = {
317
354
  }
318
355
  if (active && cfg.events.includes("session.stop")) {
319
356
  const stopHook = (input) => {
320
- void deliver("session.stop", {
357
+ deliverDetached("session.stop", {
321
358
  sessionId: input.sessionId ?? null,
322
359
  cwd: input.cwd ?? null
323
360
  });
@@ -328,7 +365,7 @@ var plugin = {
328
365
  const off = api.onPattern("tool.*", (eventName, payload) => {
329
366
  if (!/error|failed/.test(eventName)) return;
330
367
  const p = payload;
331
- void deliver("tool.error", {
368
+ deliverDetached("tool.error", {
332
369
  tool: p?.tool ?? p?.name ?? "unknown",
333
370
  busEvent: eventName,
334
371
  error: truncateText(
@@ -342,7 +379,7 @@ var plugin = {
342
379
  if (active && cfg.events.includes("budget.threshold")) {
343
380
  const off = api.onPattern("budget.*", (eventName, payload) => {
344
381
  if (!eventName.includes("threshold")) return;
345
- void deliver("budget.threshold", { busEvent: eventName, detail: payload });
382
+ deliverDetached("budget.threshold", { busEvent: eventName, detail: payload });
346
383
  });
347
384
  state.eventUnsubscribers.push(off);
348
385
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompt-firewall/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,KAAK,EAAE,MAAM,EAAa,MAAM,wBAAwB,CAAC;AA+BhE,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAcxE;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAWjG;AA0CD,KAAK,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhD,UAAU,oBAAoB;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,YAAY,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CA0B7D;AAgCD,QAAA,MAAM,MAAM,EAAE,MAkNb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompt-firewall/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,KAAK,EAAE,MAAM,EAAa,MAAM,wBAAwB,CAAC;AA2EhE,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAcxE;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAWjG;AA0CD,KAAK,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhD,UAAU,oBAAoB;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,YAAY,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CA0B7D;AAgCD,QAAA,MAAM,MAAM,EAAE,MAkNb,CAAC;eAEa,MAAM"}
@@ -1,23 +1,152 @@
1
+ // src/runtime/credential-patterns.ts
2
+ var CREDENTIAL_PATTERNS = [
3
+ // LLM provider keys
4
+ {
5
+ type: "anthropic_key",
6
+ regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
7
+ },
8
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?!ant)(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
9
+ // GitHub. `ghp_` is only the personal-access-token prefix — the OAuth
10
+ // (`gho_`), user-to-server (`ghu_`), server-to-server (`ghs_`) and
11
+ // refresh (`ghr_`) tokens grant the same or broader access and were
12
+ // previously not detected at all.
13
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
14
+ {
15
+ type: "github_oauth_token",
16
+ regex: /(?<![A-Za-z0-9])gh[ousr]_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g
17
+ },
18
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
19
+ // GitLab
20
+ { type: "gitlab_pat", regex: /(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
21
+ {
22
+ type: "gitlab_runner_token",
23
+ regex: /(?<![A-Za-z0-9])glrt-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
24
+ },
25
+ // npm — a leaked publish token is a supply-chain compromise.
26
+ { type: "npm_token", regex: /(?<![A-Za-z0-9])npm_[A-Za-z0-9]{36}(?![A-Za-z0-9])/g },
27
+ // AWS
28
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
29
+ // GCP
30
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
31
+ // Slack. `xoxe` (token-rotation) and `xapp` (app-level) were missing;
32
+ // both are as sensitive as the bot/user tokens already covered.
33
+ {
34
+ type: "slack_token",
35
+ regex: /(?<![A-Za-z0-9-])xox[abposer]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g
36
+ },
37
+ { type: "slack_app_token", regex: /(?<![A-Za-z0-9-])xapp-\d-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
38
+ {
39
+ type: "slack_webhook",
40
+ regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_-]+\/B[A-Za-z0-9_-]+\/[A-Za-z0-9]{16,}/g
41
+ },
42
+ // Stripe
43
+ {
44
+ type: "stripe_key",
45
+ regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
46
+ },
47
+ // Twilio
48
+ { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
49
+ // Telegram
50
+ {
51
+ type: "telegram_bot_token",
52
+ regex: /(?:(?<![A-Za-z0-9_])|(?<=(?:^|[^A-Za-z0-9_])bot))\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
53
+ },
54
+ // JWT
55
+ {
56
+ type: "jwt",
57
+ regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
58
+ },
59
+ // Private keys
60
+ {
61
+ type: "private_key",
62
+ regex: /(?:^|\n)(?:-----BEGIN (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----[\s\S]*?-----END (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----|-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----)(?!\S)/g
63
+ },
64
+ // AI/ML provider tokens
65
+ { type: "huggingface_token", regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g },
66
+ { type: "replicate_token", regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
67
+ { type: "perplexity_key", regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
68
+ { type: "groq_key", regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
69
+ // SaaS / infrastructure tokens — each grants API access on the user's
70
+ // account, and each was previously invisible to this gate.
71
+ { type: "sendgrid_key", regex: /(?<![A-Za-z0-9])SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
72
+ { type: "digitalocean_token", regex: /(?<![A-Za-z0-9])dop_v1_[a-f0-9]{64}(?![A-Za-z0-9])/g },
73
+ { type: "doppler_token", regex: /(?<![A-Za-z0-9])dp\.(?:pt|st|sa|scim|audit)\.[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
74
+ { type: "shopify_token", regex: /(?<![A-Za-z0-9])shp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}(?![A-Za-z0-9])/g },
75
+ { type: "docker_pat", regex: /(?<![A-Za-z0-9])dckr_pat_[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
76
+ { type: "linear_key", regex: /(?<![A-Za-z0-9])lin_api_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
77
+ { type: "atlassian_token", regex: /(?<![A-Za-z0-9])ATATT3[A-Za-z0-9_\-=]{40,}(?![A-Za-z0-9_\-=])/g },
78
+ { type: "square_token", regex: /(?<![A-Za-z0-9])(?:sq0(?:atp|csp)-|EAAA)[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
79
+ {
80
+ type: "azure_storage_key",
81
+ regex: /AccountKey=[A-Za-z0-9+/]{80,}={0,2}/g
82
+ },
83
+ {
84
+ type: "google_oauth_client_secret",
85
+ regex: /(?<![A-Za-z0-9_-])GOCSPX-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
86
+ },
87
+ // Bearer tokens
88
+ {
89
+ type: "bearer_token",
90
+ regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
91
+ },
92
+ // Database URIs. Require password-bearing user-info; credential-free values stay scannable.
93
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
94
+ {
95
+ type: "postgres_uri",
96
+ // Query parsers decode percent-encoded parameter names. Recognize each
97
+ // encoded character in `password` so mixed forms such as `pass%77ord`
98
+ // cannot bypass detection while keeping the scan strictly bounded.
99
+ regex: /postgres(?:ql)?:\/\/(?:[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+|[^&\s?"'`#]{1,2048}\?(?:(?!(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=)[^&\s#"'`]{1,256}&){0,32}(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=[^&\s#"'`]{1,4096})/g
100
+ },
101
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
102
+ { type: "redis_uri", regex: /redis:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g }
103
+ ];
104
+ function cloneCredentialPatterns() {
105
+ return CREDENTIAL_PATTERNS.map((p) => ({
106
+ type: p.type,
107
+ regex: new RegExp(p.regex.source, p.regex.flags)
108
+ }));
109
+ }
110
+
1
111
  // src/prompt-firewall/index.ts
2
- var PATTERNS = [
3
- { kind: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
112
+ var KIND_ALIASES = {
113
+ aws_access_key: "aws-access-key",
114
+ private_key: "private-key-block",
115
+ github_pat: "github-token",
116
+ github_oauth_token: "github-token",
117
+ openai_key: "openai-key",
118
+ anthropic_key: "anthropic-key",
119
+ slack_token: "slack-token",
120
+ gcp_key: "google-api-key",
121
+ bearer_token: "bearer-token"
122
+ };
123
+ var EXTRA_PATTERNS = [
4
124
  {
125
+ // AWS secret access keys have no prefix — only a 40-char base64-ish
126
+ // shape — so they need a nearby `aws`/`secret` mention to be worth
127
+ // acting on.
128
+ //
129
+ // The previous spelling required the word "aws" to appear AFTER the
130
+ // key and wrapped the run in `\b`. Both are wrong: the real layout is
131
+ // `AWS_SECRET_ACCESS_KEY=<key>` (context first), and a `\b` cannot
132
+ // hold next to the `+` or `/` that base64 keys routinely end with. The
133
+ // pattern therefore never fired on a real key. Anchor on the context
134
+ // token instead and let the key follow it.
5
135
  kind: "aws-secret-key",
6
- re: /\b(?<![A-Za-z0-9/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])\b(?=.*aws)/gi
136
+ re: /(?:aws|amazon)[A-Za-z0-9_-]*(?:secret|key)[A-Za-z0-9_-]*\s*[:=]\s*['"]?([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+])/gi
7
137
  },
8
- { kind: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g },
9
- { kind: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
10
- { kind: "openai-key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
11
- { kind: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
12
- { kind: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
13
- { kind: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
14
- { kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
15
- { kind: "bearer-token", re: /\bBearer\s+[A-Za-z0-9._-]{20,}\b/g },
16
138
  {
17
139
  kind: "generic-secret-assignment",
18
140
  re: /\b(?:api[_-]?key|secret|password|passwd|token)\s*[:=]\s*['"]?[A-Za-z0-9._/+-]{12,}['"]?/gi
19
141
  }
20
142
  ];
143
+ var PATTERNS = [
144
+ ...cloneCredentialPatterns().map((p) => ({
145
+ kind: KIND_ALIASES[p.type] ?? p.type,
146
+ re: p.regex
147
+ })),
148
+ ...EXTRA_PATTERNS
149
+ ];
21
150
  function detectSecrets(text, allow) {
22
151
  const counts = /* @__PURE__ */ new Map();
23
152
  for (const p of PATTERNS) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/refactor-suggester/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AASrD,MAAM,MAAM,SAAS,GACjB,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,aAAa,CAAC;AAElB,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAgOD,QAAA,MAAM,MAAM,EAAE,MAoOb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/refactor-suggester/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AASrD,MAAM,MAAM,SAAS,GACjB,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,aAAa,CAAC;AASlB,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AA8PD,QAAA,MAAM,MAAM,EAAE,MAwOb,CAAC;eAEa,MAAM"}