aislop 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url";
12
12
  import os from "node:os";
13
13
  import ts from "typescript";
14
14
  import wcwidth from "wcwidth";
15
+ import crypto from "node:crypto";
15
16
  import { isCancel, multiselect, select, text } from "@clack/prompts";
16
17
 
17
18
  //#region \0rolldown/runtime.js
@@ -982,7 +983,7 @@ const detectSwallowedExceptions = async (context) => {
982
983
  };
983
984
 
984
985
  //#endregion
985
- //#region src/engines/ai-slop/narrative-comments.ts
986
+ //#region src/engines/ai-slop/narrative-comments-patterns.ts
986
987
  const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
987
988
  const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
988
989
  const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
@@ -1041,6 +1042,19 @@ const SUPPORTED_EXTS = new Set([
1041
1042
  ".java",
1042
1043
  ".php"
1043
1044
  ]);
1045
+ const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
1046
+ const EXPORT_DEFAULT = /^\s*export\s+default\b/;
1047
+ const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
1048
+ const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
1049
+ const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
1050
+ const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
1051
+ const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
1052
+ const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
1053
+ const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
1054
+ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
1055
+
1056
+ //#endregion
1057
+ //#region src/engines/ai-slop/narrative-comments.ts
1044
1058
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
1045
1059
  const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
1046
1060
  const getCommentSyntax = (ext) => {
@@ -1132,16 +1146,6 @@ const collectBlocks = (sourceLines, syntax) => {
1132
1146
  }
1133
1147
  return blocks;
1134
1148
  };
1135
- const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
1136
- const EXPORT_DEFAULT = /^\s*export\s+default\b/;
1137
- const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
1138
- const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
1139
- const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
1140
- const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
1141
- const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
1142
- const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
1143
- const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
1144
- const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
1145
1149
  const looksLikeDeclarationPreamble = (nextLine, ext) => {
1146
1150
  if (nextLine === null) return false;
1147
1151
  if (DECL_START.test(nextLine) || EXPORT_DEFAULT.test(nextLine)) return true;
@@ -1682,72 +1686,12 @@ const architectureEngine = {
1682
1686
  };
1683
1687
 
1684
1688
  //#endregion
1685
- //#region src/engines/code-quality/complexity.ts
1686
- const FUNCTION_PATTERNS = [
1687
- {
1688
- regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
1689
- langFilter: [
1690
- ".js",
1691
- ".ts",
1692
- ".jsx",
1693
- ".tsx",
1694
- ".mjs",
1695
- ".cjs"
1696
- ]
1697
- },
1698
- {
1699
- regex: /^\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?:=>|:\s*\w)/,
1700
- langFilter: [
1701
- ".js",
1702
- ".ts",
1703
- ".jsx",
1704
- ".tsx",
1705
- ".mjs",
1706
- ".cjs"
1707
- ]
1708
- },
1709
- {
1710
- regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
1711
- langFilter: [".py"]
1712
- },
1713
- {
1714
- regex: /^\s*func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(([^)]*)\)/,
1715
- langFilter: [".go"]
1716
- },
1717
- {
1718
- regex: /^\s*fn\s+(\w+)\s*\(([^)]*)\)/,
1719
- langFilter: [".rs"]
1720
- },
1721
- {
1722
- regex: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)(\w+)\s*\(([^)]*)\)/,
1723
- langFilter: [
1724
- ".java",
1725
- ".cs",
1726
- ".cpp",
1727
- ".c",
1728
- ".php"
1729
- ]
1730
- }
1731
- ];
1732
- const countParams = (p) => p.trim() ? p.split(",").length : 0;
1733
- const matchFunctionOnLine = (line, ext) => {
1734
- for (let i = 0; i < FUNCTION_PATTERNS.length; i++) {
1735
- const pattern = FUNCTION_PATTERNS[i];
1736
- if (!pattern.langFilter.includes(ext)) continue;
1737
- const match = line.match(pattern.regex);
1738
- if (match) return {
1739
- name: match[1],
1740
- params: match[2] ?? "",
1741
- patternIndex: i
1742
- };
1743
- }
1744
- return null;
1745
- };
1689
+ //#region src/engines/code-quality/function-boundaries.ts
1746
1690
  const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
1747
- const findFunctionEnd = (lines, startIndex, isPython) => {
1748
- if (isPython) return findPythonFunctionEnd(lines, startIndex);
1749
- return findBraceFunctionEnd(lines, startIndex);
1750
- };
1691
+ const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
1692
+ const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
1693
+ const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
1694
+ const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
1751
1695
  const isControlFlowBrace = (lineText, braceIndex) => {
1752
1696
  const before = lineText.substring(0, braceIndex).trimEnd();
1753
1697
  if (before.endsWith(")")) return true;
@@ -1827,16 +1771,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
1827
1771
  maxNesting
1828
1772
  };
1829
1773
  };
1830
- const isDataFile = (content) => {
1831
- const nonEmpty = content.split("\n").filter((l) => l.trim().length > 0);
1832
- if (nonEmpty.length === 0) return false;
1833
- const dataLinePattern = /^\s*[{}[\]"']/;
1834
- return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
1774
+ const findFunctionEnd = (lines, startIndex, isPython) => {
1775
+ if (isPython) return findPythonFunctionEnd(lines, startIndex);
1776
+ return findBraceFunctionEnd(lines, startIndex);
1835
1777
  };
1836
- const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
1837
- const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
1838
- const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
1839
- const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
1840
1778
  const isBlockArrow = (lines, startIndex) => {
1841
1779
  if (ARROW_BLOCK_RE.test(lines[startIndex])) return true;
1842
1780
  if (ARROW_END_RE.test(lines[startIndex])) {
@@ -1872,6 +1810,75 @@ const countTemplateLines = (bodyLines) => {
1872
1810
  }
1873
1811
  return templateLineCount;
1874
1812
  };
1813
+
1814
+ //#endregion
1815
+ //#region src/engines/code-quality/complexity.ts
1816
+ const FUNCTION_PATTERNS = [
1817
+ {
1818
+ regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
1819
+ langFilter: [
1820
+ ".js",
1821
+ ".ts",
1822
+ ".jsx",
1823
+ ".tsx",
1824
+ ".mjs",
1825
+ ".cjs"
1826
+ ]
1827
+ },
1828
+ {
1829
+ regex: /^\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?:=>|:\s*\w)/,
1830
+ langFilter: [
1831
+ ".js",
1832
+ ".ts",
1833
+ ".jsx",
1834
+ ".tsx",
1835
+ ".mjs",
1836
+ ".cjs"
1837
+ ]
1838
+ },
1839
+ {
1840
+ regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
1841
+ langFilter: [".py"]
1842
+ },
1843
+ {
1844
+ regex: /^\s*func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(([^)]*)\)/,
1845
+ langFilter: [".go"]
1846
+ },
1847
+ {
1848
+ regex: /^\s*fn\s+(\w+)\s*\(([^)]*)\)/,
1849
+ langFilter: [".rs"]
1850
+ },
1851
+ {
1852
+ regex: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)(\w+)\s*\(([^)]*)\)/,
1853
+ langFilter: [
1854
+ ".java",
1855
+ ".cs",
1856
+ ".cpp",
1857
+ ".c",
1858
+ ".php"
1859
+ ]
1860
+ }
1861
+ ];
1862
+ const countParams = (p) => p.trim() ? p.split(",").length : 0;
1863
+ const matchFunctionOnLine = (line, ext) => {
1864
+ for (let i = 0; i < FUNCTION_PATTERNS.length; i++) {
1865
+ const pattern = FUNCTION_PATTERNS[i];
1866
+ if (!pattern.langFilter.includes(ext)) continue;
1867
+ const match = line.match(pattern.regex);
1868
+ if (match) return {
1869
+ name: match[1],
1870
+ params: match[2] ?? "",
1871
+ patternIndex: i
1872
+ };
1873
+ }
1874
+ return null;
1875
+ };
1876
+ const isDataFile = (content) => {
1877
+ const nonEmpty = content.split("\n").filter((l) => l.trim().length > 0);
1878
+ if (nonEmpty.length === 0) return false;
1879
+ const dataLinePattern = /^\s*[{}[\]"']/;
1880
+ return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
1881
+ };
1875
1882
  const analyzeFunctions = (content, ext) => {
1876
1883
  const lines = content.split("\n");
1877
1884
  const functions = [];
@@ -1894,13 +1901,14 @@ const analyzeFunctions = (content, ext) => {
1894
1901
  }
1895
1902
  return functions;
1896
1903
  };
1904
+ const JSX_FILE_LOC_MULTIPLIER = 1.5;
1897
1905
  const checkFileDiagnostics = (relativePath, content, limits) => {
1898
1906
  const results = [];
1899
1907
  const lineCount = content.split("\n").length;
1900
1908
  const ext = path.extname(relativePath).toLowerCase();
1901
1909
  if (isDataFile(content)) return results;
1902
- const effectiveMax = ext === ".jsx" || ext === ".tsx" ? limits.maxFileLoc * 2 : limits.maxFileLoc;
1903
- if (lineCount > Math.ceil(effectiveMax * 1.1)) results.push({
1910
+ const effectiveMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
1911
+ if (lineCount > effectiveMax) results.push({
1904
1912
  filePath: relativePath,
1905
1913
  engine: "code-quality",
1906
1914
  rule: "complexity/file-too-large",
@@ -3581,6 +3589,216 @@ const runCargoAudit = async (rootDir, timeout) => {
3581
3589
  }
3582
3590
  };
3583
3591
 
3592
+ //#endregion
3593
+ //#region src/utils/source-masker.ts
3594
+ const JS_EXTS = new Set([
3595
+ ".ts",
3596
+ ".tsx",
3597
+ ".js",
3598
+ ".jsx",
3599
+ ".mjs",
3600
+ ".cjs"
3601
+ ]);
3602
+ const PY_EXTS = new Set([".py"]);
3603
+ const RB_EXTS = new Set([".rb"]);
3604
+ const PHP_EXTS = new Set([".php"]);
3605
+ const familyForExt = (ext) => {
3606
+ if (JS_EXTS.has(ext)) return "js";
3607
+ if (PY_EXTS.has(ext)) return "py";
3608
+ if (RB_EXTS.has(ext)) return "rb";
3609
+ if (PHP_EXTS.has(ext)) return "php";
3610
+ return "none";
3611
+ };
3612
+ const maskStringsAndComments = (content, ext) => {
3613
+ const family = familyForExt(ext);
3614
+ if (family === "none") return content;
3615
+ if (family === "js") return maskJs(content);
3616
+ return maskSimple(content, family);
3617
+ };
3618
+ const maskJs = (content) => {
3619
+ const out = content.split("");
3620
+ const len = content.length;
3621
+ const tplStack = [];
3622
+ let i = 0;
3623
+ const mask = (start, end) => {
3624
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
3625
+ };
3626
+ while (i < len) {
3627
+ const c = content[i];
3628
+ const next = content[i + 1];
3629
+ if (tplStack.length > 0) {
3630
+ if (c === "{") {
3631
+ tplStack[tplStack.length - 1]++;
3632
+ i++;
3633
+ continue;
3634
+ }
3635
+ if (c === "}") {
3636
+ if (tplStack[tplStack.length - 1] === 0) {
3637
+ tplStack.pop();
3638
+ const scan = consumeTemplateString(content, i + 1);
3639
+ mask(i + 1, scan.maskEnd);
3640
+ if (scan.openedInterp) tplStack.push(0);
3641
+ i = scan.resumeAt;
3642
+ continue;
3643
+ }
3644
+ tplStack[tplStack.length - 1]--;
3645
+ i++;
3646
+ continue;
3647
+ }
3648
+ if (c === "\"" || c === "'") {
3649
+ const strStart = i;
3650
+ i = consumeQuotedString(content, i, c);
3651
+ mask(strStart + 1, i - 1);
3652
+ continue;
3653
+ }
3654
+ if (c === "`") {
3655
+ const scan = consumeTemplateString(content, i + 1);
3656
+ mask(i + 1, scan.maskEnd);
3657
+ if (scan.openedInterp) tplStack.push(0);
3658
+ i = scan.resumeAt;
3659
+ continue;
3660
+ }
3661
+ if (c === "/" && next === "/") {
3662
+ const strStart = i;
3663
+ while (i < len && content[i] !== "\n") i++;
3664
+ mask(strStart, i);
3665
+ continue;
3666
+ }
3667
+ if (c === "/" && next === "*") {
3668
+ const strStart = i;
3669
+ i += 2;
3670
+ while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
3671
+ if (i < len - 1) i += 2;
3672
+ mask(strStart, i);
3673
+ continue;
3674
+ }
3675
+ i++;
3676
+ continue;
3677
+ }
3678
+ if (c === "\"" || c === "'") {
3679
+ const strStart = i;
3680
+ i = consumeQuotedString(content, i, c);
3681
+ mask(strStart + 1, i - 1);
3682
+ continue;
3683
+ }
3684
+ if (c === "`") {
3685
+ const scan = consumeTemplateString(content, i + 1);
3686
+ mask(i + 1, scan.maskEnd);
3687
+ if (scan.openedInterp) tplStack.push(0);
3688
+ i = scan.resumeAt;
3689
+ continue;
3690
+ }
3691
+ if (c === "/" && next === "/") {
3692
+ const strStart = i;
3693
+ while (i < len && content[i] !== "\n") i++;
3694
+ mask(strStart, i);
3695
+ continue;
3696
+ }
3697
+ if (c === "/" && next === "*") {
3698
+ const strStart = i;
3699
+ i += 2;
3700
+ while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
3701
+ if (i < len - 1) i += 2;
3702
+ mask(strStart, i);
3703
+ continue;
3704
+ }
3705
+ i++;
3706
+ }
3707
+ return out.join("");
3708
+ };
3709
+ const consumeQuotedString = (content, start, quote) => {
3710
+ const len = content.length;
3711
+ let i = start + 1;
3712
+ while (i < len) {
3713
+ const c = content[i];
3714
+ if (c === "\\" && i + 1 < len) {
3715
+ i += 2;
3716
+ continue;
3717
+ }
3718
+ if (c === quote) return i + 1;
3719
+ if (c === "\n") return i;
3720
+ i++;
3721
+ }
3722
+ return i;
3723
+ };
3724
+ const consumeTemplateString = (content, start) => {
3725
+ const len = content.length;
3726
+ let i = start;
3727
+ while (i < len) {
3728
+ const c = content[i];
3729
+ if (c === "\\" && i + 1 < len) {
3730
+ i += 2;
3731
+ continue;
3732
+ }
3733
+ if (c === "`") return {
3734
+ maskEnd: i,
3735
+ resumeAt: i + 1,
3736
+ openedInterp: false
3737
+ };
3738
+ if (c === "$" && content[i + 1] === "{") return {
3739
+ maskEnd: i,
3740
+ resumeAt: i + 2,
3741
+ openedInterp: true
3742
+ };
3743
+ i++;
3744
+ }
3745
+ return {
3746
+ maskEnd: i,
3747
+ resumeAt: i,
3748
+ openedInterp: false
3749
+ };
3750
+ };
3751
+ const maskSimple = (content, family) => {
3752
+ const out = content.split("");
3753
+ const len = content.length;
3754
+ let i = 0;
3755
+ const mask = (start, end) => {
3756
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
3757
+ };
3758
+ while (i < len) {
3759
+ const c = content[i];
3760
+ const next = content[i + 1];
3761
+ if (family === "py" && (c === "\"" || c === "'")) {
3762
+ if (content[i + 1] === c && content[i + 2] === c) {
3763
+ const triple = c + c + c;
3764
+ const end = content.indexOf(triple, i + 3);
3765
+ const stop = end === -1 ? len : end + 3;
3766
+ mask(i + 3, stop - 3);
3767
+ i = stop;
3768
+ continue;
3769
+ }
3770
+ }
3771
+ if (c === "\"" || c === "'") {
3772
+ const strStart = i;
3773
+ i = consumeQuotedString(content, i, c);
3774
+ mask(strStart + 1, i - 1);
3775
+ continue;
3776
+ }
3777
+ if ((family === "py" || family === "rb" || family === "php") && c === "#") {
3778
+ const strStart = i;
3779
+ while (i < len && content[i] !== "\n") i++;
3780
+ mask(strStart, i);
3781
+ continue;
3782
+ }
3783
+ if (family === "php" && c === "/" && next === "/") {
3784
+ const strStart = i;
3785
+ while (i < len && content[i] !== "\n") i++;
3786
+ mask(strStart, i);
3787
+ continue;
3788
+ }
3789
+ if (family === "php" && c === "/" && next === "*") {
3790
+ const strStart = i;
3791
+ i += 2;
3792
+ while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
3793
+ if (i < len - 1) i += 2;
3794
+ mask(strStart, i);
3795
+ continue;
3796
+ }
3797
+ i++;
3798
+ }
3799
+ return out.join("");
3800
+ };
3801
+
3584
3802
  //#endregion
3585
3803
  //#region src/engines/security/risky.ts
3586
3804
  const ev = "eval";
@@ -3710,12 +3928,13 @@ const detectRiskyConstructs = async (context) => {
3710
3928
  const relativePath = path.relative(context.rootDirectory, filePath);
3711
3929
  const normalizedPath = relativePath.split(path.sep).join("/");
3712
3930
  const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
3931
+ const masked = maskStringsAndComments(content, ext);
3713
3932
  for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
3714
3933
  if (!extensions.includes(ext)) continue;
3715
3934
  if (isMigrationOrSeeder && name === "sql-injection") continue;
3716
3935
  const regex = new RegExp(pattern.source, pattern.flags);
3717
3936
  let match;
3718
- while ((match = regex.exec(content)) !== null) {
3937
+ while ((match = regex.exec(masked)) !== null) {
3719
3938
  const line = content.slice(0, match.index).split("\n").length;
3720
3939
  if (name === "innerhtml") {
3721
3940
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
@@ -4472,7 +4691,7 @@ const getStagedFiles = (cwd) => {
4472
4691
  * Application version — injected at build time by tsdown from package.json.
4473
4692
  * The fallback should always match the "version" field in package.json.
4474
4693
  */
4475
- const APP_VERSION = "0.5.0";
4694
+ const APP_VERSION = "0.5.1";
4476
4695
 
4477
4696
  //#endregion
4478
4697
  //#region src/utils/telemetry.ts
@@ -6071,42 +6290,40 @@ const runExpoDoctor = async (context) => {
6071
6290
 
6072
6291
  //#endregion
6073
6292
  //#region src/commands/fix-force.ts
6074
- const getJsAuditFixCommand = (rootDirectory) => {
6075
- if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return {
6076
- command: "pnpm",
6077
- args: ["audit", "--fix"]
6078
- };
6079
- if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return {
6080
- command: "npm",
6081
- args: ["audit", "fix"]
6082
- };
6293
+ const INSTALL_TIMEOUT = 1800 * 1e3;
6294
+ const AUDIT_TIMEOUT = 60 * 1e3;
6295
+ const detectPackageManager = (rootDirectory) => {
6296
+ if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return "pnpm";
6297
+ if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return "npm";
6083
6298
  return null;
6084
6299
  };
6085
- const INSTALL_TIMEOUT = 1800 * 1e3;
6086
6300
  const fixDependencyAudit = async (context, onProgress) => {
6087
- const auditFix = getJsAuditFixCommand(context.rootDirectory);
6088
- if (!auditFix) return;
6089
- onProgress?.(`Dependency audit fixes · running ${auditFix.command} audit fix (can take a few minutes)`);
6090
- const result = await runSubprocess(auditFix.command, auditFix.args, {
6091
- cwd: context.rootDirectory,
6301
+ const pm = detectPackageManager(context.rootDirectory);
6302
+ if (!pm) return;
6303
+ if (pm === "npm") {
6304
+ await runNpmAuditFix(context.rootDirectory, onProgress);
6305
+ await tryNpmOverrides(context.rootDirectory, onProgress);
6306
+ return;
6307
+ }
6308
+ await tryPnpmOverrides(context.rootDirectory, onProgress);
6309
+ };
6310
+ const runNpmAuditFix = async (rootDir, onProgress) => {
6311
+ onProgress?.("Dependency audit fixes · running npm audit fix (can take a few minutes)");
6312
+ const result = await runSubprocess("npm", ["audit", "fix"], {
6313
+ cwd: rootDir,
6092
6314
  timeout: INSTALL_TIMEOUT
6093
6315
  });
6094
- if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error(`${auditFix.command} audit fix failed`);
6095
- onProgress?.(`Dependency audit fixes · running ${auditFix.command} install`);
6096
- const installResult = await runSubprocess(auditFix.command, ["install"], {
6097
- cwd: context.rootDirectory,
6316
+ if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error("npm audit fix failed");
6317
+ onProgress?.("Dependency audit fixes · running npm install");
6318
+ const installResult = await runSubprocess("npm", ["install"], {
6319
+ cwd: rootDir,
6098
6320
  timeout: INSTALL_TIMEOUT
6099
6321
  });
6100
- if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || `${auditFix.command} install failed after audit fix`);
6101
- if (auditFix.command === "npm") await tryNpmOverrides(context.rootDirectory, onProgress);
6322
+ if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || "npm install failed after audit fix");
6102
6323
  };
6103
- /**
6104
- * For unresolvable transitive vulnerabilities, attempt to add npm overrides
6105
- * in package.json. This forces a newer version of the vulnerable transitive dep.
6106
- */
6107
- const fetchLatestVersion = async (rootDir, pkgName) => {
6324
+ const fetchLatestVersion = async (rootDir, pkgName, pm) => {
6108
6325
  try {
6109
- const result = await runSubprocess("npm", [
6326
+ const result = await runSubprocess(pm, [
6110
6327
  "view",
6111
6328
  pkgName,
6112
6329
  "version",
@@ -6120,11 +6337,11 @@ const fetchLatestVersion = async (rootDir, pkgName) => {
6120
6337
  return null;
6121
6338
  }
6122
6339
  };
6123
- const collectOverrides = async (rootDir, vulnerabilities) => {
6340
+ const collectNpmOverrides = async (rootDir, vulnerabilities) => {
6124
6341
  const overrides = {};
6125
6342
  for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
6126
6343
  if (vuln.fixAvailable !== false || !vuln.range) continue;
6127
- const latest = await fetchLatestVersion(rootDir, pkgName);
6344
+ const latest = await fetchLatestVersion(rootDir, pkgName, "npm");
6128
6345
  if (latest) overrides[pkgName] = latest;
6129
6346
  }
6130
6347
  return overrides;
@@ -6133,12 +6350,12 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
6133
6350
  try {
6134
6351
  const auditResult = await runSubprocess("npm", ["audit", "--json"], {
6135
6352
  cwd: rootDir,
6136
- timeout: 3e4
6353
+ timeout: AUDIT_TIMEOUT
6137
6354
  });
6138
6355
  if (!auditResult.stdout) return;
6139
6356
  const vulnerabilities = JSON.parse(auditResult.stdout).vulnerabilities;
6140
6357
  if (!vulnerabilities) return;
6141
- const overrides = await collectOverrides(rootDir, vulnerabilities);
6358
+ const overrides = await collectNpmOverrides(rootDir, vulnerabilities);
6142
6359
  if (Object.keys(overrides).length === 0) return;
6143
6360
  const pkgPath = path.join(rootDir, "package.json");
6144
6361
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
@@ -6146,7 +6363,7 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
6146
6363
  ...pkg.overrides || {},
6147
6364
  ...overrides
6148
6365
  };
6149
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
6366
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
6150
6367
  onProgress?.("Dependency audit fixes · applying npm overrides (npm install)");
6151
6368
  await runSubprocess("npm", ["install"], {
6152
6369
  cwd: rootDir,
@@ -6154,6 +6371,61 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
6154
6371
  });
6155
6372
  } catch {}
6156
6373
  };
6374
+ const patchedRangeToVersion = (patched) => {
6375
+ const match = patched.match(/^\s*>=?\s*([0-9]+\.[0-9]+\.[0-9]+[^\s]*)/);
6376
+ return match ? `^${match[1]}` : null;
6377
+ };
6378
+ const overrideKey = (name, vulnerable, patched) => {
6379
+ if (vulnerable && vulnerable.trim().length > 0 && !/^\*$/.test(vulnerable.trim())) return `${name}@${vulnerable.trim()}`;
6380
+ const first = patched.match(/([0-9]+\.[0-9]+\.[0-9]+)/)?.[1];
6381
+ return first ? `${name}@<${first}` : name;
6382
+ };
6383
+ const collectPnpmOverrides = (advisories) => {
6384
+ const overrides = {};
6385
+ for (const adv of Object.values(advisories)) {
6386
+ if (!adv.module_name || !adv.patched_versions) continue;
6387
+ const target = patchedRangeToVersion(adv.patched_versions);
6388
+ if (!target) continue;
6389
+ const key = overrideKey(adv.module_name, adv.vulnerable_versions, adv.patched_versions);
6390
+ overrides[key] = target;
6391
+ }
6392
+ return overrides;
6393
+ };
6394
+ const tryPnpmOverrides = async (rootDir, onProgress) => {
6395
+ onProgress?.("Dependency audit fixes · running pnpm audit");
6396
+ const auditResult = await runSubprocess("pnpm", ["audit", "--json"], {
6397
+ cwd: rootDir,
6398
+ timeout: AUDIT_TIMEOUT
6399
+ });
6400
+ if (!auditResult.stdout) return;
6401
+ let parsed;
6402
+ try {
6403
+ parsed = JSON.parse(auditResult.stdout);
6404
+ } catch {
6405
+ return;
6406
+ }
6407
+ const advisories = parsed.advisories;
6408
+ if (!advisories || Object.keys(advisories).length === 0) return;
6409
+ const overrides = collectPnpmOverrides(advisories);
6410
+ if (Object.keys(overrides).length === 0) return;
6411
+ const pkgPath = path.join(rootDir, "package.json");
6412
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
6413
+ const pnpmBlock = pkg.pnpm ?? {};
6414
+ const existing = pnpmBlock.overrides ?? {};
6415
+ pkg.pnpm = {
6416
+ ...pnpmBlock,
6417
+ overrides: {
6418
+ ...existing,
6419
+ ...overrides
6420
+ }
6421
+ };
6422
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
6423
+ onProgress?.("Dependency audit fixes · applying pnpm overrides (pnpm install)");
6424
+ await runSubprocess("pnpm", ["install"], {
6425
+ cwd: rootDir,
6426
+ timeout: INSTALL_TIMEOUT
6427
+ });
6428
+ };
6157
6429
  const fixExpoDependencies = async (context, onProgress) => {
6158
6430
  await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
6159
6431
  onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
@@ -6178,10 +6450,6 @@ const fixExpoDependencies = async (context, onProgress) => {
6178
6450
  });
6179
6451
  if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
6180
6452
  };
6181
- /**
6182
- * Run expo-doctor to detect packages that should not be installed directly,
6183
- * then uninstall them. No hardcoded list — expo-doctor is the source of truth.
6184
- */
6185
6453
  const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
6186
6454
  try {
6187
6455
  onProgress?.("Expo dependency alignment · running expo-doctor");
@@ -6268,8 +6536,9 @@ const runFormattingStep = async (deps) => {
6268
6536
  const runForceSteps = async (deps) => {
6269
6537
  if (!deps.force) return;
6270
6538
  if (deps.config.engines["code-quality"] && hasJsOrTs(deps.projectInfo)) await deps.runStep("Remove unused files", () => runKnipUnusedFiles(deps.resolvedDir), () => fixUnusedFiles(deps.resolvedDir));
6271
- if (deps.config.engines.security) await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, deps.rail.setActiveLabel));
6272
- if (deps.projectInfo.frameworks.includes("expo")) await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, deps.rail.setActiveLabel));
6539
+ const railUpdate = (label) => deps.rail.setActiveLabel(label);
6540
+ if (deps.config.engines.security) await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, railUpdate));
6541
+ if (deps.projectInfo.frameworks.includes("expo")) await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, railUpdate));
6273
6542
  };
6274
6543
 
6275
6544
  //#endregion
@@ -6433,6 +6702,344 @@ const fixCommand = async (directory, config, options = {
6433
6702
  }
6434
6703
  };
6435
6704
 
6705
+ //#endregion
6706
+ //#region src/hooks/feedback.ts
6707
+ const MAX_FINDINGS = 20;
6708
+ const toFinding = (d, rootDirectory) => {
6709
+ if (d.severity !== "error" && d.severity !== "warning") return null;
6710
+ const file = path.isAbsolute(d.filePath) ? path.relative(rootDirectory, d.filePath) : d.filePath;
6711
+ return {
6712
+ ruleId: d.rule,
6713
+ severity: d.severity,
6714
+ category: d.category,
6715
+ file,
6716
+ line: d.line,
6717
+ col: d.column || void 0,
6718
+ message: d.message
6719
+ };
6720
+ };
6721
+ const buildNextSteps = (findings) => {
6722
+ const steps = [];
6723
+ const errorCount = findings.filter((f) => f.severity === "error").length;
6724
+ if (errorCount > 0) steps.push(`Fix ${errorCount} error${errorCount === 1 ? "" : "s"} before the next turn.`);
6725
+ const byFile = /* @__PURE__ */ new Map();
6726
+ for (const f of findings) {
6727
+ const list = byFile.get(f.file) ?? [];
6728
+ list.push(f);
6729
+ byFile.set(f.file, list);
6730
+ }
6731
+ for (const [file, list] of Array.from(byFile.entries()).slice(0, 3)) {
6732
+ const lines = list.map((f) => f.line).slice(0, 3).join(", ");
6733
+ steps.push(`Address ${list.length} finding${list.length === 1 ? "" : "s"} in ${file} (line${list.length === 1 ? "" : "s"} ${lines}).`);
6734
+ }
6735
+ return steps;
6736
+ };
6737
+ const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
6738
+ const all = diagnostics.map((d) => toFinding(d, rootDirectory)).filter((x) => x !== null);
6739
+ const capped = all.slice(0, MAX_FINDINGS);
6740
+ const elided = all.length > MAX_FINDINGS ? all.length - MAX_FINDINGS : void 0;
6741
+ const counts = {
6742
+ error: diagnostics.filter((d) => d.severity === "error").length,
6743
+ warning: diagnostics.filter((d) => d.severity === "warning").length,
6744
+ fixable: diagnostics.filter((d) => d.fixable).length,
6745
+ total: all.length
6746
+ };
6747
+ return {
6748
+ schema: "aislop.hook.v1",
6749
+ score,
6750
+ baseline,
6751
+ regressed: typeof baseline === "number" ? score < baseline : false,
6752
+ counts,
6753
+ findings: capped,
6754
+ elided,
6755
+ nextSteps: buildNextSteps(capped)
6756
+ };
6757
+ };
6758
+
6759
+ //#endregion
6760
+ //#region src/hooks/adapters/claude.ts
6761
+ const extractFiles = (stdin) => {
6762
+ const files = /* @__PURE__ */ new Set();
6763
+ const input = stdin.tool_input ?? {};
6764
+ if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
6765
+ if (Array.isArray(input.edits)) {
6766
+ for (const e of input.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
6767
+ }
6768
+ return Array.from(files);
6769
+ };
6770
+ const parseClaudeStdin = (raw) => {
6771
+ if (!raw.trim()) return {};
6772
+ try {
6773
+ return JSON.parse(raw);
6774
+ } catch {
6775
+ return {};
6776
+ }
6777
+ };
6778
+ const readStdin = async () => {
6779
+ if (process.stdin.isTTY) return "";
6780
+ const chunks = [];
6781
+ for await (const chunk of process.stdin) chunks.push(chunk);
6782
+ return Buffer.concat(chunks).toString("utf-8");
6783
+ };
6784
+ const existingAbsolutePaths = (cwd, files) => files.map((f) => path.isAbsolute(f) ? f : path.join(cwd, f)).filter((p) => {
6785
+ try {
6786
+ return fs.statSync(p).isFile();
6787
+ } catch {
6788
+ return false;
6789
+ }
6790
+ });
6791
+ const runScopedScan = async (cwd, filePaths) => {
6792
+ const project = await discoverProject(cwd);
6793
+ const config = loadConfig(cwd);
6794
+ const configDir = findConfigDir(project.rootDirectory);
6795
+ const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
6796
+ const diagnostics = (await runEngines({
6797
+ rootDirectory: project.rootDirectory,
6798
+ languages: project.languages,
6799
+ frameworks: project.frameworks,
6800
+ files: filterProjectFiles(project.rootDirectory, filePaths),
6801
+ installedTools: project.installedTools,
6802
+ config: {
6803
+ quality: config.quality,
6804
+ security: {
6805
+ audit: false,
6806
+ auditTimeout: 0
6807
+ },
6808
+ architectureRulesPath: config.engines.architecture ? rulesPath : void 0
6809
+ }
6810
+ }, {
6811
+ format: config.engines.format,
6812
+ lint: config.engines.lint,
6813
+ "code-quality": config.engines["code-quality"],
6814
+ "ai-slop": config.engines["ai-slop"],
6815
+ architecture: config.engines.architecture,
6816
+ security: false
6817
+ })).flatMap((r) => r.diagnostics);
6818
+ const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
6819
+ return {
6820
+ diagnostics,
6821
+ score,
6822
+ rootDirectory: project.rootDirectory
6823
+ };
6824
+ };
6825
+ const renderClaudeOutput = (additional, block) => {
6826
+ const out = { hookSpecificOutput: {
6827
+ hookEventName: "PostToolUse",
6828
+ additionalContext: additional
6829
+ } };
6830
+ if (block) {
6831
+ out.decision = "block";
6832
+ out.reason = block.reason;
6833
+ }
6834
+ return out;
6835
+ };
6836
+ const runClaudeHook = async (deps = {}) => {
6837
+ const getStdin = deps.stdin ?? readStdin;
6838
+ const write = deps.write ?? ((s) => process.stdout.write(s));
6839
+ const input = parseClaudeStdin(await getStdin());
6840
+ const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
6841
+ const absFiles = existingAbsolutePaths(cwd, extractFiles(input));
6842
+ if (absFiles.length === 0) return 0;
6843
+ try {
6844
+ const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, absFiles);
6845
+ const feedback = buildFeedback(diagnostics, score, rootDirectory);
6846
+ const envelope = renderClaudeOutput(JSON.stringify(feedback));
6847
+ write(JSON.stringify(envelope));
6848
+ return 0;
6849
+ } catch {
6850
+ return 0;
6851
+ }
6852
+ };
6853
+
6854
+ //#endregion
6855
+ //#region src/hooks/assets.ts
6856
+ const AISLOP_MD_BODY = `# aislop — agent instructions
6857
+
6858
+ [aislop](https://github.com/heavykenny/aislop) is your guardrail for shipping clean, maintainable, production-grade code. It catches the patterns AI coding tools leave behind: dead imports, redundant code, duplicate logic, unsafe any-typed casts, narrative comments, swallowed errors, oversized functions, risky security constructs — and grades the project out of 100 so regressions are visible. Treat its findings as blocking, not advisory: the goal is code a human maintainer would happily inherit.
6859
+
6860
+ ## On every edit
6861
+
6862
+ A PostToolUse hook runs \`aislop hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`AislopFeedback\` (schema \`aislop.hook.v1\`). Act on them the same turn.
6863
+
6864
+ ## Severity ladder
6865
+
6866
+ - \`error\` — MUST fix this turn.
6867
+ - \`warning\` + \`fixable: true\` — MUST fix this turn.
6868
+ - \`warning\` + \`fixable: false\` — fix if trivially mechanical, otherwise surface in your reply.
6869
+
6870
+ ## Rules
6871
+
6872
+ - \`.aislop/config.yaml\` — thresholds and engine toggles. Treat as authoritative; don't edit without user consent.
6873
+ - \`.aislop/rules.yaml\` — project-specific architecture rules (may be absent). When a finding cites \`architecture/*\`, open this file and follow it.
6874
+ - Custom rules can change between sessions. Trust what the scan returns, not a cached understanding of what the rules are.
6875
+
6876
+ ## Principles
6877
+
6878
+ - Do not disable rules to pass the scan. Fix the underlying issue.
6879
+ - If a finding is a false positive, leave it and explain in your reply — do not delete the rule config.
6880
+ - The findings payload includes \`nextSteps[]\` — treat those as your plan for the turn.
6881
+ `;
6882
+
6883
+ //#endregion
6884
+ //#region src/hooks/io/atomic-write.ts
6885
+ const atomicWrite = (targetPath, content) => {
6886
+ const dir = path.dirname(targetPath);
6887
+ fs.mkdirSync(dir, { recursive: true });
6888
+ const rand = Math.random().toString(36).slice(2, 10);
6889
+ const tmp = path.join(dir, `.aislop-tmp-${process.pid}-${rand}`);
6890
+ fs.writeFileSync(tmp, content, "utf-8");
6891
+ fs.renameSync(tmp, targetPath);
6892
+ };
6893
+ const readIfExists = (targetPath) => {
6894
+ try {
6895
+ return fs.readFileSync(targetPath, "utf-8");
6896
+ } catch {
6897
+ return null;
6898
+ }
6899
+ };
6900
+
6901
+ //#endregion
6902
+ //#region src/hooks/io/json-patch.ts
6903
+ const AISLOP_SENTINEL_KEY = "__aislop";
6904
+ const isAislopManaged = (x) => typeof x === "object" && x !== null && AISLOP_SENTINEL_KEY in x && x[AISLOP_SENTINEL_KEY] != null;
6905
+ const groupIsAislop = (group) => {
6906
+ if (typeof group !== "object" || group === null) return false;
6907
+ const hooks = group.hooks;
6908
+ if (!Array.isArray(hooks)) return false;
6909
+ return hooks.some((h) => isAislopManaged(h));
6910
+ };
6911
+ const upsertHookGroup = (config, event, group) => {
6912
+ const next = { ...config };
6913
+ const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
6914
+ const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((g) => !groupIsAislop(g));
6915
+ next.hooks = {
6916
+ ...hooks,
6917
+ [event]: [...cleaned, group]
6918
+ };
6919
+ return next;
6920
+ };
6921
+
6922
+ //#endregion
6923
+ //#region src/hooks/io/sentinel.ts
6924
+ const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
6925
+ const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
6926
+ const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
6927
+ const renderFence = (body, hash) => [
6928
+ `<!-- aislop:begin v1 hash=${hash} -->`,
6929
+ body.trimEnd(),
6930
+ "<!-- aislop:end v1 -->"
6931
+ ].join("\n");
6932
+ const upsertMarkdownFence = (existing, body, hash) => {
6933
+ const fenced = renderFence(body, hash);
6934
+ if (existing == null || existing.length === 0) return {
6935
+ nextContent: `${fenced}\n`,
6936
+ replaced: false
6937
+ };
6938
+ const begin = existing.match(BEGIN_RE);
6939
+ const end = existing.match(END_RE);
6940
+ if (begin && end && (end.index ?? 0) > (begin.index ?? 0)) return {
6941
+ nextContent: `${existing.slice(0, begin.index)}${fenced}${existing.slice((end.index ?? 0) + end[0].length)}`,
6942
+ replaced: true
6943
+ };
6944
+ return {
6945
+ nextContent: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${fenced}\n`,
6946
+ replaced: false
6947
+ };
6948
+ };
6949
+
6950
+ //#endregion
6951
+ //#region src/hooks/install/claude.ts
6952
+ const resolveClaudePaths = (home) => ({
6953
+ settings: path.join(home, ".claude", "settings.json"),
6954
+ aislopMd: path.join(home, ".claude", "AISLOP.md"),
6955
+ claudeMd: path.join(home, ".claude", "CLAUDE.md")
6956
+ });
6957
+ const buildHookGroup = () => {
6958
+ const hashBody = JSON.stringify({
6959
+ command: "aislop hook claude",
6960
+ matcher: "Edit|Write|MultiEdit"
6961
+ });
6962
+ return {
6963
+ matcher: "Edit|Write|MultiEdit",
6964
+ hooks: [{
6965
+ type: "command",
6966
+ command: "aislop hook claude",
6967
+ [AISLOP_SENTINEL_KEY]: {
6968
+ v: 1,
6969
+ managed: true,
6970
+ hash: sentinelHash(hashBody)
6971
+ }
6972
+ }]
6973
+ };
6974
+ };
6975
+ const installClaude = (opts = { home: os.homedir() }) => {
6976
+ const paths = resolveClaudePaths(opts.home);
6977
+ const wrote = [];
6978
+ const skipped = [];
6979
+ const existingSettingsRaw = readIfExists(paths.settings);
6980
+ let settingsObj = {};
6981
+ if (existingSettingsRaw) try {
6982
+ settingsObj = JSON.parse(existingSettingsRaw);
6983
+ } catch {
6984
+ atomicWrite(`${paths.settings}.aislop-bak`, existingSettingsRaw);
6985
+ }
6986
+ const nextSettings = upsertHookGroup(settingsObj, "PostToolUse", buildHookGroup());
6987
+ const nextSettingsStr = `${JSON.stringify(nextSettings, null, 2)}\n`;
6988
+ if (nextSettingsStr !== existingSettingsRaw) {
6989
+ atomicWrite(paths.settings, nextSettingsStr);
6990
+ wrote.push(paths.settings);
6991
+ } else skipped.push(paths.settings);
6992
+ const mdHash = sentinelHash(AISLOP_MD_BODY);
6993
+ const existingMd = readIfExists(paths.aislopMd);
6994
+ const fenced = upsertMarkdownFence(existingMd, AISLOP_MD_BODY, mdHash);
6995
+ if (fenced.nextContent !== existingMd) {
6996
+ atomicWrite(paths.aislopMd, fenced.nextContent);
6997
+ wrote.push(paths.aislopMd);
6998
+ } else skipped.push(paths.aislopMd);
6999
+ const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
7000
+ const marker = "@AISLOP.md";
7001
+ if (!existingClaudeMd.includes(marker)) {
7002
+ const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
7003
+ const nextClaudeMd = `${existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`}${marker}\n`;
7004
+ atomicWrite(paths.claudeMd, nextClaudeMd);
7005
+ wrote.push(paths.claudeMd);
7006
+ } else skipped.push(paths.claudeMd);
7007
+ return {
7008
+ wrote,
7009
+ skipped
7010
+ };
7011
+ };
7012
+
7013
+ //#endregion
7014
+ //#region src/commands/hook.ts
7015
+ const hookInstall = async (opts) => {
7016
+ if (opts.agent !== "claude") {
7017
+ process.stderr.write(`hook install: agent "${opts.agent}" not implemented yet\n`);
7018
+ process.exitCode = 1;
7019
+ return;
7020
+ }
7021
+ if (!opts.global) {
7022
+ process.stderr.write("hook install: only --global supported in this release\n");
7023
+ process.exitCode = 1;
7024
+ return;
7025
+ }
7026
+ const result = installClaude({ home: os.homedir() });
7027
+ if (result.wrote.length === 0) {
7028
+ process.stdout.write("hook install: nothing to do (already up to date)\n");
7029
+ return;
7030
+ }
7031
+ for (const f of result.wrote) process.stdout.write(`wrote ${f}\n`);
7032
+ for (const f of result.skipped) process.stdout.write(`skip ${f}\n`);
7033
+ };
7034
+ const hookRun = async (agent) => {
7035
+ if (agent !== "claude") {
7036
+ process.stderr.write(`hook: agent "${agent}" not implemented yet\n`);
7037
+ process.exit(0);
7038
+ }
7039
+ const exitCode = await runClaudeHook();
7040
+ process.exit(exitCode);
7041
+ };
7042
+
6436
7043
  //#endregion
6437
7044
  //#region src/commands/init.ts
6438
7045
  const buildInitSuccessRender = (input) => {
@@ -7004,6 +7611,16 @@ program.command("ci [directory]").description("CI-friendly JSON output with exit
7004
7611
  program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
7005
7612
  await rulesCommand(directory);
7006
7613
  });
7614
+ const hook = program.command("hook").description("Install or invoke AI-agent integration hooks");
7615
+ hook.command("install").description("Install an agent-integration hook (Claude Code supported)").option("--agent <name>", "target agent (claude)", "claude").option("-g, --global", "install to the user-scope config", true).action(async (opts) => {
7616
+ await hookInstall({
7617
+ agent: opts.agent,
7618
+ global: Boolean(opts.global)
7619
+ });
7620
+ });
7621
+ hook.command("claude").description("Internal: Claude Code PostToolUse callback (reads stdin)").action(async () => {
7622
+ await hookRun("claude");
7623
+ });
7007
7624
  const main = async () => {
7008
7625
  await program.parseAsync();
7009
7626
  await flushTelemetry();