semlint-cli 0.1.5 → 0.1.7

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/diff.js ADDED
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseDiffGitHeader = parseDiffGitHeader;
4
+ exports.splitDiffIntoFileChunks = splitDiffIntoFileChunks;
5
+ function unquoteDiffPath(raw) {
6
+ if (raw.startsWith("\"") && raw.endsWith("\"") && raw.length >= 2) {
7
+ return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
8
+ }
9
+ return raw;
10
+ }
11
+ function parseDiffGitHeader(line) {
12
+ const match = line.match(/^diff --git (?:"a\/((?:[^"\\]|\\.)+)"|a\/(\S+)) (?:"b\/((?:[^"\\]|\\.)+)"|b\/(\S+))$/);
13
+ if (!match) {
14
+ return undefined;
15
+ }
16
+ const aRaw = match[1] ?? match[2];
17
+ const bRaw = match[3] ?? match[4];
18
+ if (!aRaw || !bRaw) {
19
+ return undefined;
20
+ }
21
+ return {
22
+ aPath: unquoteDiffPath(aRaw),
23
+ bPath: unquoteDiffPath(bRaw)
24
+ };
25
+ }
26
+ function splitDiffIntoFileChunks(diff) {
27
+ const lines = diff.split("\n");
28
+ const chunks = [];
29
+ let currentLines = [];
30
+ let currentFile = "";
31
+ const flush = () => {
32
+ if (currentLines.length === 0) {
33
+ return;
34
+ }
35
+ chunks.push({
36
+ file: currentFile,
37
+ chunk: currentLines.join("\n")
38
+ });
39
+ currentLines = [];
40
+ currentFile = "";
41
+ };
42
+ for (const line of lines) {
43
+ if (line.startsWith("diff --git ")) {
44
+ flush();
45
+ const parsed = parseDiffGitHeader(line);
46
+ if (parsed) {
47
+ currentFile = parsed.bPath;
48
+ }
49
+ }
50
+ currentLines.push(line);
51
+ }
52
+ flush();
53
+ return chunks;
54
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runBatchDispatch = runBatchDispatch;
4
+ exports.runParallelDispatch = runParallelDispatch;
5
+ const diagnostics_1 = require("./diagnostics");
6
+ const filter_1 = require("./filter");
7
+ const prompts_1 = require("./prompts");
8
+ const utils_1 = require("./utils");
9
+ function buildBatchPrompt(rules, diff) {
10
+ const ruleBlocks = rules
11
+ .map((rule) => [
12
+ `RULE_ID: ${rule.id}`,
13
+ `RULE_TITLE: ${rule.title}`,
14
+ `SEVERITY_DEFAULT: ${rule.effectiveSeverity}`,
15
+ "INSTRUCTIONS:",
16
+ rule.prompt
17
+ ].join("\n"))
18
+ .join("\n\n---\n\n");
19
+ return (0, prompts_1.renderBatchPrompt)({ ruleBlocks, diff });
20
+ }
21
+ async function runBatchDispatch(input) {
22
+ const { rules, diff, changedFiles, backend, config, repoRoot } = input;
23
+ const diagnostics = [];
24
+ let backendErrors = 0;
25
+ (0, utils_1.debugLog)(config.debug, `Running ${rules.length} rule(s) in batch mode`);
26
+ const combinedDiff = rules
27
+ .map((rule) => (0, filter_1.buildScopedDiff)(rule, diff, changedFiles))
28
+ .filter((chunk) => chunk.trim() !== "")
29
+ .join("\n");
30
+ const batchPrompt = buildBatchPrompt(rules, combinedDiff || diff);
31
+ try {
32
+ const batchResult = await backend.runPrompt({
33
+ label: "Batch",
34
+ prompt: batchPrompt,
35
+ timeoutMs: config.timeoutMs
36
+ });
37
+ const groupedByRule = batchResult.diagnostics.reduce((acc, diagnostic) => {
38
+ if (typeof diagnostic === "object" &&
39
+ diagnostic !== null &&
40
+ !Array.isArray(diagnostic) &&
41
+ typeof diagnostic.rule_id === "string") {
42
+ const ruleId = diagnostic.rule_id;
43
+ acc.set(ruleId, [...(acc.get(ruleId) ?? []), diagnostic]);
44
+ return acc;
45
+ }
46
+ (0, utils_1.debugLog)(config.debug, "Batch: dropped diagnostic without valid rule_id");
47
+ return acc;
48
+ }, new Map());
49
+ const validRuleIds = new Set(rules.map((rule) => rule.id));
50
+ Array.from(groupedByRule.keys())
51
+ .filter((ruleId) => !validRuleIds.has(ruleId))
52
+ .forEach((ruleId) => {
53
+ (0, utils_1.debugLog)(config.debug, `Batch: dropped diagnostic for unknown rule_id ${ruleId}`);
54
+ });
55
+ const normalized = rules.flatMap((rule) => (0, diagnostics_1.normalizeDiagnostics)(rule.id, groupedByRule.get(rule.id) ?? [], config.debug, repoRoot));
56
+ diagnostics.push(...normalized);
57
+ }
58
+ catch (error) {
59
+ backendErrors += 1;
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ (0, utils_1.debugLog)(config.debug, `Batch backend error: ${message}`);
62
+ }
63
+ return { diagnostics, backendErrors };
64
+ }
65
+ async function runParallelDispatch(input) {
66
+ const { rules, diff, changedFiles, backend, config, repoRoot } = input;
67
+ (0, utils_1.debugLog)(config.debug, `Running ${rules.length} rule(s) in parallel`);
68
+ const runResults = await Promise.all(rules.map(async (rule) => {
69
+ const ruleStartedAt = Date.now();
70
+ (0, utils_1.debugLog)(config.debug, `Rule ${rule.id}: started`);
71
+ const scopedDiff = (0, filter_1.buildScopedDiff)(rule, diff, changedFiles);
72
+ const prompt = (0, filter_1.buildRulePrompt)(rule, scopedDiff);
73
+ try {
74
+ const result = await backend.runRule({
75
+ ruleId: rule.id,
76
+ prompt,
77
+ timeoutMs: config.timeoutMs
78
+ });
79
+ const normalized = (0, diagnostics_1.normalizeDiagnostics)(rule.id, result.diagnostics, config.debug, repoRoot);
80
+ (0, utils_1.debugLog)(config.debug, `Rule ${rule.id}: finished in ${Date.now() - ruleStartedAt}ms`);
81
+ return { backendError: false, normalized };
82
+ }
83
+ catch (error) {
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ (0, utils_1.debugLog)(config.debug, `Backend error for rule ${rule.id}: ${message}`);
86
+ (0, utils_1.debugLog)(config.debug, `Rule ${rule.id}: finished in ${Date.now() - ruleStartedAt}ms`);
87
+ return { backendError: true, normalized: [] };
88
+ }
89
+ }));
90
+ return {
91
+ diagnostics: runResults.flatMap((result) => result.normalized),
92
+ backendErrors: runResults.filter((result) => result.backendError).length
93
+ };
94
+ }
package/dist/filter.js CHANGED
@@ -8,28 +8,9 @@ exports.getRuleCandidateFiles = getRuleCandidateFiles;
8
8
  exports.shouldRunRule = shouldRunRule;
9
9
  exports.buildScopedDiff = buildScopedDiff;
10
10
  exports.buildRulePrompt = buildRulePrompt;
11
+ const diff_1 = require("./diff");
11
12
  const picomatch_1 = __importDefault(require("picomatch"));
12
- function unquoteDiffPath(raw) {
13
- if (raw.startsWith("\"") && raw.endsWith("\"") && raw.length >= 2) {
14
- return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
15
- }
16
- return raw;
17
- }
18
- function parseDiffGitHeader(line) {
19
- const match = line.match(/^diff --git (?:"a\/((?:[^"\\]|\\.)+)"|a\/(\S+)) (?:"b\/((?:[^"\\]|\\.)+)"|b\/(\S+))$/);
20
- if (!match) {
21
- return undefined;
22
- }
23
- const aRaw = match[1] ?? match[2];
24
- const bRaw = match[3] ?? match[4];
25
- if (!aRaw || !bRaw) {
26
- return undefined;
27
- }
28
- return {
29
- aPath: unquoteDiffPath(aRaw),
30
- bPath: unquoteDiffPath(bRaw)
31
- };
32
- }
13
+ const prompts_1 = require("./prompts");
33
14
  function extractChangedFilesFromDiff(diff) {
34
15
  const files = new Set();
35
16
  const lines = diff.split("\n");
@@ -37,7 +18,7 @@ function extractChangedFilesFromDiff(diff) {
37
18
  if (!line.startsWith("diff --git ")) {
38
19
  continue;
39
20
  }
40
- const parsed = parseDiffGitHeader(line);
21
+ const parsed = (0, diff_1.parseDiffGitHeader)(line);
41
22
  if (!parsed) {
42
23
  continue;
43
24
  }
@@ -48,9 +29,6 @@ function extractChangedFilesFromDiff(diff) {
48
29
  }
49
30
  return Array.from(files);
50
31
  }
51
- function matchesAnyGlob(filePath, globs) {
52
- return globs.some((glob) => (0, picomatch_1.default)(glob)(filePath));
53
- }
54
32
  function matchesAnyRegex(diff, regexes) {
55
33
  for (const candidate of regexes) {
56
34
  try {
@@ -67,14 +45,16 @@ function matchesAnyRegex(diff, regexes) {
67
45
  }
68
46
  function getRuleCandidateFiles(rule, changedFiles) {
69
47
  let fileCandidates = changedFiles;
70
- if (rule.include_globs && rule.include_globs.length > 0) {
71
- fileCandidates = changedFiles.filter((filePath) => matchesAnyGlob(filePath, rule.include_globs));
48
+ const includeMatcher = rule.include_globs && rule.include_globs.length > 0 ? (0, picomatch_1.default)(rule.include_globs) : null;
49
+ const excludeMatcher = rule.exclude_globs && rule.exclude_globs.length > 0 ? (0, picomatch_1.default)(rule.exclude_globs) : null;
50
+ if (includeMatcher) {
51
+ fileCandidates = changedFiles.filter((filePath) => includeMatcher(filePath));
72
52
  if (fileCandidates.length === 0) {
73
53
  return [];
74
54
  }
75
55
  }
76
- if (rule.exclude_globs && rule.exclude_globs.length > 0) {
77
- fileCandidates = fileCandidates.filter((filePath) => !matchesAnyGlob(filePath, rule.exclude_globs));
56
+ if (excludeMatcher) {
57
+ fileCandidates = fileCandidates.filter((filePath) => !excludeMatcher(filePath));
78
58
  }
79
59
  return fileCandidates;
80
60
  }
@@ -88,41 +68,12 @@ function shouldRunRule(rule, changedFiles, diff) {
88
68
  }
89
69
  return true;
90
70
  }
91
- function splitDiffIntoFileChunks(diff) {
92
- const lines = diff.split("\n");
93
- const chunks = [];
94
- let currentLines = [];
95
- let currentFile = "";
96
- const flush = () => {
97
- if (currentLines.length === 0) {
98
- return;
99
- }
100
- chunks.push({
101
- file: currentFile,
102
- chunk: currentLines.join("\n")
103
- });
104
- currentLines = [];
105
- currentFile = "";
106
- };
107
- for (const line of lines) {
108
- if (line.startsWith("diff --git ")) {
109
- flush();
110
- const parsed = parseDiffGitHeader(line);
111
- if (parsed) {
112
- currentFile = parsed.bPath;
113
- }
114
- }
115
- currentLines.push(line);
116
- }
117
- flush();
118
- return chunks;
119
- }
120
71
  function buildScopedDiff(rule, fullDiff, changedFiles) {
121
72
  const candidateFiles = new Set(getRuleCandidateFiles(rule, changedFiles));
122
73
  if (candidateFiles.size === 0) {
123
74
  return fullDiff;
124
75
  }
125
- const chunks = splitDiffIntoFileChunks(fullDiff);
76
+ const chunks = (0, diff_1.splitDiffIntoFileChunks)(fullDiff);
126
77
  const scoped = chunks
127
78
  .filter((chunk) => chunk.file !== "" && candidateFiles.has(chunk.file))
128
79
  .map((chunk) => chunk.chunk)
@@ -130,41 +81,11 @@ function buildScopedDiff(rule, fullDiff, changedFiles) {
130
81
  return scoped.trim() === "" ? fullDiff : scoped;
131
82
  }
132
83
  function buildRulePrompt(rule, diff) {
133
- return [
134
- "You are Semlint, an expert semantic code reviewer.",
135
- "Analyze ONLY the modified code present in the DIFF below.",
136
- "Return JSON only (no markdown, no prose, no code fences).",
137
- "Output schema:",
138
- "{",
139
- " \"diagnostics\": [",
140
- " {",
141
- " \"rule_id\": string,",
142
- " \"severity\": \"error\" | \"warn\" | \"info\",",
143
- " \"message\": string,",
144
- " \"file\": string,",
145
- " \"line\": number,",
146
- " \"column\"?: number,",
147
- " \"end_line\"?: number,",
148
- " \"end_column\"?: number,",
149
- " \"evidence\"?: string,",
150
- " \"confidence\"?: number",
151
- " }",
152
- " ]",
153
- "}",
154
- "Rules:",
155
- "- If there are no findings, return {\"diagnostics\":[]}.",
156
- "- Each diagnostic must reference a changed file from the DIFF.",
157
- "- Use the provided RULE_ID exactly in every diagnostic.",
158
- "- Keep messages concise and actionable.",
159
- "",
160
- `RULE_ID: ${rule.id}`,
161
- `RULE_TITLE: ${rule.title}`,
162
- `SEVERITY_DEFAULT: ${rule.effectiveSeverity}`,
163
- "",
164
- "INSTRUCTIONS:",
165
- rule.prompt,
166
- "",
167
- "DIFF:",
84
+ return (0, prompts_1.renderRulePrompt)({
85
+ ruleId: rule.id,
86
+ ruleTitle: rule.title,
87
+ severityDefault: rule.effectiveSeverity,
88
+ instructions: rule.prompt,
168
89
  diff
169
- ].join("\n");
90
+ });
170
91
  }
package/dist/init.js CHANGED
@@ -8,6 +8,23 @@ const picocolors_1 = __importDefault(require("picocolors"));
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const node_child_process_1 = require("node:child_process");
11
+ const SCAFFOLD_BACKENDS = {
12
+ "cursor-cli": {
13
+ executable: "cursor",
14
+ args: ["agent", "{prompt}", "--model", "{model}", "--print", "--mode", "ask", "--output-format", "text"],
15
+ model: "auto"
16
+ },
17
+ "claude-code": {
18
+ executable: "claude",
19
+ args: ["{prompt}", "--model", "{model}", "--output-format", "json"],
20
+ model: "auto"
21
+ },
22
+ "codex-cli": {
23
+ executable: "codex",
24
+ args: ["{prompt}", "--model", "{model}"],
25
+ model: "auto"
26
+ }
27
+ };
11
28
  function commandExists(command) {
12
29
  const result = (0, node_child_process_1.spawnSync)(command, ["--version"], {
13
30
  stdio: "ignore"
@@ -34,12 +51,21 @@ function detectBackend() {
34
51
  ];
35
52
  for (const candidate of candidates) {
36
53
  if (commandExists(candidate.executable)) {
37
- return candidate;
54
+ const scaffold = SCAFFOLD_BACKENDS[candidate.backend];
55
+ return {
56
+ backend: candidate.backend,
57
+ executable: scaffold.executable,
58
+ args: scaffold.args,
59
+ model: scaffold.model,
60
+ reason: candidate.reason
61
+ };
38
62
  }
39
63
  }
40
64
  return {
41
65
  backend: "cursor-cli",
42
- executable: "cursor",
66
+ executable: SCAFFOLD_BACKENDS["cursor-cli"].executable,
67
+ args: SCAFFOLD_BACKENDS["cursor-cli"].args,
68
+ model: SCAFFOLD_BACKENDS["cursor-cli"].model,
43
69
  reason: "no known agent CLI detected, using default Cursor setup"
44
70
  };
45
71
  }
@@ -52,7 +78,6 @@ function scaffoldConfig(force = false) {
52
78
  const detected = detectBackend();
53
79
  const scaffold = {
54
80
  backend: detected.backend,
55
- model: "auto",
56
81
  budgets: {
57
82
  timeout_ms: 120000
58
83
  },
@@ -62,13 +87,21 @@ function scaffoldConfig(force = false) {
62
87
  execution: {
63
88
  batch: false
64
89
  },
90
+ security: {
91
+ secret_guard: true,
92
+ allow_patterns: [],
93
+ ignore_files: [".gitignore", ".cursorignore", ".semlintignore"],
94
+ allow_files: []
95
+ },
65
96
  rules: {
66
97
  disable: [],
67
98
  severity_overrides: {}
68
99
  },
69
100
  backends: {
70
101
  [detected.backend]: {
71
- executable: detected.executable
102
+ executable: detected.executable,
103
+ args: detected.args,
104
+ model: detected.model
72
105
  }
73
106
  }
74
107
  };
@@ -80,16 +113,23 @@ function scaffoldConfig(force = false) {
80
113
  node_fs_1.default.mkdirSync(rulesDir, { recursive: true });
81
114
  process.stdout.write(picocolors_1.default.green(`Created ${node_path_1.default.join(".semlint", "rules")}/\n`));
82
115
  }
83
- const exampleRulePath = node_path_1.default.join(rulesDir, "SEMLINT_EXAMPLE_001.json");
84
- if (!node_fs_1.default.existsSync(exampleRulePath)) {
85
- const exampleRule = {
86
- id: "SEMLINT_EXAMPLE_001",
87
- title: "My first rule",
88
- severity_default: "warn",
89
- prompt: "Describe what the agent should check in the changed code. Example: flag when new functions lack JSDoc, or when error handling is missing."
90
- };
91
- node_fs_1.default.writeFileSync(exampleRulePath, `${JSON.stringify(exampleRule, null, 2)}\n`, "utf8");
92
- process.stdout.write(picocolors_1.default.green(`Created ${node_path_1.default.join(".semlint", "rules", "SEMLINT_EXAMPLE_001.json")} `) + picocolors_1.default.dim(`(edit the title and prompt to define your rule)\n`));
116
+ const bundledRulesDir = node_path_1.default.resolve(__dirname, "..", ".semlint", "rules");
117
+ if (!node_fs_1.default.existsSync(bundledRulesDir) || !node_fs_1.default.statSync(bundledRulesDir).isDirectory()) {
118
+ process.stderr.write(picocolors_1.default.yellow(`No bundled rules found at ${bundledRulesDir}. Add rule files manually under ${node_path_1.default.join(".semlint", "rules")}.\n`));
119
+ return 0;
120
+ }
121
+ const bundledRules = node_fs_1.default
122
+ .readdirSync(bundledRulesDir)
123
+ .filter((name) => name.endsWith(".json"))
124
+ .sort((a, b) => a.localeCompare(b));
125
+ for (const fileName of bundledRules) {
126
+ const source = node_path_1.default.join(bundledRulesDir, fileName);
127
+ const target = node_path_1.default.join(rulesDir, fileName);
128
+ if (!force && node_fs_1.default.existsSync(target)) {
129
+ continue;
130
+ }
131
+ node_fs_1.default.copyFileSync(source, target);
132
+ process.stdout.write(picocolors_1.default.green(`Copied ${node_path_1.default.join(".semlint", "rules", fileName)}\n`));
93
133
  }
94
134
  return 0;
95
135
  }