@stupify/cli 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -31
- package/dist/analysis.d.ts +11 -9
- package/dist/analysis.js +30 -173
- package/dist/checks.d.ts +1 -0
- package/dist/checks.js +89 -2
- package/dist/command.js +55 -91
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/counter-scout.js +70 -8
- package/dist/doctor.d.ts +4 -0
- package/dist/doctor.js +131 -0
- package/dist/git.d.ts +4 -1
- package/dist/git.js +34 -0
- package/dist/hooks.d.ts +3 -0
- package/dist/hooks.js +117 -0
- package/dist/model.d.ts +1 -15
- package/dist/model.js +37 -21
- package/dist/prompts.d.ts +8 -5
- package/dist/prompts.js +58 -168
- package/dist/render.d.ts +2 -2
- package/dist/render.js +70 -78
- package/dist/repomix-provider.d.ts +10 -2
- package/dist/repomix-provider.js +62 -11
- package/dist/search-bench.d.ts +1 -0
- package/dist/search-bench.js +675 -0
- package/dist/search-profile.d.ts +6 -0
- package/dist/search-profile.js +73 -0
- package/dist/sem-provider.d.ts +2 -2
- package/dist/sem-provider.js +33 -7
- package/dist/stupify.d.ts +2 -0
- package/dist/stupify.js +183 -333
- package/dist/types.d.ts +193 -109
- package/package.json +1 -1
- package/src/analysis.ts +48 -268
- package/src/checks.ts +91 -2
- package/src/command.ts +62 -107
- package/src/constants.ts +1 -1
- package/src/counter-scout.ts +63 -7
- package/src/doctor.ts +140 -0
- package/src/git.ts +35 -1
- package/src/hooks.ts +134 -0
- package/src/model.ts +39 -26
- package/src/prompts.ts +66 -202
- package/src/render.ts +68 -79
- package/src/repomix-provider.ts +66 -10
- package/src/search-bench.ts +783 -0
- package/src/search-profile.ts +89 -0
- package/src/sem-provider.ts +36 -9
- package/src/stupify.ts +213 -526
- package/src/types.ts +195 -119
- package/dist/batcher.d.ts +0 -3
- package/dist/batcher.js +0 -142
- package/dist/candidate-context.d.ts +0 -2
- package/dist/candidate-context.js +0 -40
- package/dist/experiment.d.ts +0 -1
- package/dist/experiment.js +0 -225
- package/src/batcher.ts +0 -198
- package/src/candidate-context.ts +0 -43
- package/src/experiment.ts +0 -317
package/src/render.ts
CHANGED
|
@@ -1,44 +1,42 @@
|
|
|
1
1
|
import { VERSION } from "./constants.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SearchCommand, SearchRunJson } from "./types.ts";
|
|
3
3
|
|
|
4
|
-
export function
|
|
5
|
-
if (command.json)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
export function renderSearchRun(run: SearchRunJson, command: SearchCommand): string {
|
|
5
|
+
if (command.json) return JSON.stringify(run, null, 2);
|
|
6
|
+
|
|
7
|
+
if (run.stats.skipped && run.stats.skipReason === "input_too_large") {
|
|
8
|
+
return `🧙 stupify 🪄
|
|
9
|
+
Search input is too large for precise local search.
|
|
10
|
+
Size:
|
|
11
|
+
~${run.stats.inputTokens ?? "unknown"} tokens
|
|
12
|
+
Limit:
|
|
13
|
+
${run.stats.inputTokenCap ?? "unknown"} tokens
|
|
14
|
+
Stupify skipped the search rather than review truncated context.
|
|
15
|
+
Nothing was blocked.
|
|
16
|
+
Try:
|
|
17
|
+
stupify ${sourceHint(command)} --max-search-input-tokens ${Math.max((run.stats.inputTokens ?? 12_000) + 1, (run.stats.inputTokenCap ?? 12_000) * 2)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (run.stats.skipped && run.stats.skipReason === "no_candidates") {
|
|
21
|
+
return `🧙 stupify 🪄
|
|
22
|
+
Search complete.
|
|
23
|
+
Patterns: ${run.patterns.join(", ")}
|
|
24
|
+
No search targets found.`;
|
|
14
25
|
}
|
|
15
26
|
|
|
16
|
-
if (
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
${report.run.auditedCandidateCount} candidates inspected
|
|
22
|
-
${report.result.findings.length} findings
|
|
23
|
-
${renderAuditStats(report)}
|
|
24
|
-
${renderWarnings(report)}
|
|
25
|
-
Findings:
|
|
26
|
-
${renderFindings(report)}
|
|
27
|
-
Timing:
|
|
28
|
-
total_ms=${report.run.timingsMs.total} entity_diff_ms=${report.run.timingsMs.diff} model_ms=${report.run.timingsMs.modelLoad} scout_ms=${report.run.timingsMs.search} context_audit_ms=${report.run.timingsMs.audit}`;
|
|
27
|
+
if (run.matches.length === 0) {
|
|
28
|
+
return `🧙 stupify 🪄
|
|
29
|
+
Search complete.
|
|
30
|
+
Patterns: ${run.patterns.join(", ")}
|
|
31
|
+
No judgment-offload signals found.`;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
${renderWarnings(report)}
|
|
38
|
-
Findings:
|
|
39
|
-
${renderFindings(report)}
|
|
40
|
-
Timing:
|
|
41
|
-
total_ms=${report.run.timingsMs.total} diff_ms=${report.run.timingsMs.diff} model_ms=${report.run.timingsMs.modelLoad} search_ms=${report.run.timingsMs.search} audit_ms=${report.run.timingsMs.audit}`;
|
|
34
|
+
return `🧙 stupify 🪄
|
|
35
|
+
Possible judgment-offload detected:
|
|
36
|
+
${run.matches.map((match, index) => `${index + 1}. ${match.patternId}
|
|
37
|
+
${match.reason}
|
|
38
|
+
Proof: ${match.proof}`).join("\n")}
|
|
39
|
+
Search mode is warn-only.`;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
export function helpText(): string {
|
|
@@ -49,59 +47,50 @@ Usage:
|
|
|
49
47
|
stupify --since "2 weeks ago"
|
|
50
48
|
stupify --commit <commit>
|
|
51
49
|
stupify --commits <count>
|
|
52
|
-
stupify
|
|
50
|
+
stupify --staged
|
|
51
|
+
stupify --mode search --staged
|
|
52
|
+
stupify hook install|uninstall|status
|
|
53
|
+
stupify doctor
|
|
54
|
+
stupify bench search experiments/search-bench.json
|
|
53
55
|
git diff HEAD~1..HEAD | stupify --stdin
|
|
54
56
|
|
|
55
57
|
Options:
|
|
56
|
-
--
|
|
57
|
-
--
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
--
|
|
61
|
-
--
|
|
62
|
-
--
|
|
63
|
-
|
|
64
|
-
--
|
|
65
|
-
|
|
66
|
-
--
|
|
67
|
-
--
|
|
68
|
-
--
|
|
69
|
-
|
|
70
|
-
--
|
|
71
|
-
|
|
72
|
-
--
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
--json Print JSON only.
|
|
58
|
+
--staged Search staged changes.
|
|
59
|
+
--mode <mode> search. Search is the only analysis mode.
|
|
60
|
+
--since <date> Search the net diff from the first commit before this git date to HEAD.
|
|
61
|
+
--commit <commit> Search one commit as a net diff.
|
|
62
|
+
--commits <count> Search the net diff across the last N non-merge commits.
|
|
63
|
+
--stdin Read a git diff from stdin.
|
|
64
|
+
--debug-sem Print sem commands and stderr.
|
|
65
|
+
--max-candidates <n> Max semantic search targets. Default: 10.
|
|
66
|
+
--max-search-input-tokens <n>
|
|
67
|
+
Max search input tokens before skipping. Default: 12000.
|
|
68
|
+
--checks <ids> Comma-separated pattern ids.
|
|
69
|
+
--model <id> gemma-4-e2b, gemma-4-e4b, gemma-4-26b-a4b, qwen3-4b-magicquant, qwen2.5-coder-1.5b, qwen2.5-coder-7b, or qwen2.5-coder-32b.
|
|
70
|
+
--search-profile <path>
|
|
71
|
+
Dev/bench-only search profile override.
|
|
72
|
+
--include-counter-reason-in-prompt
|
|
73
|
+
Debug/bench-only: include counter reason in the model prompt.
|
|
74
|
+
--json Print JSON only.
|
|
75
|
+
|
|
76
|
+
Diagnostics:
|
|
77
|
+
stupify doctor Check local setup, hook status, and privacy boundary.
|
|
77
78
|
|
|
78
79
|
Default:
|
|
79
80
|
stupify is equivalent to stupify --since "2 weeks ago".
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function renderFindings(report: AnalysisReport): string {
|
|
87
|
-
if (report.result.findings.length === 0) return " None.";
|
|
82
|
+
Pipeline:
|
|
83
|
+
sem diff -> counter scout -> Repomix context -> local search model.
|
|
88
84
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
${finding.why}
|
|
92
|
-
Proof: ${finding.proof}`)
|
|
93
|
-
.join("\n");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function renderWarnings(report: AnalysisReport): string {
|
|
97
|
-
if (report.run.warnings.length === 0) return "";
|
|
98
|
-
return `Warnings:
|
|
99
|
-
${report.run.warnings.map((warning) => ` ${warning}`).join("\n")}
|
|
85
|
+
Not included:
|
|
86
|
+
Findings audit, validators, judges, baselines, sharing, hosted server calls, GitHub, dashboards, or repo-wide crawling.
|
|
100
87
|
`;
|
|
101
88
|
}
|
|
102
89
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
90
|
+
function sourceHint(command: SearchCommand): string {
|
|
91
|
+
if (command.kind === "staged") return "--staged";
|
|
92
|
+
if (command.kind === "since") return `--since "${command.since}"`;
|
|
93
|
+
if (command.kind === "commit") return `--commit ${command.commit}`;
|
|
94
|
+
if (command.kind === "commits") return `--commits ${command.count}`;
|
|
95
|
+
return "--stdin";
|
|
107
96
|
}
|
package/src/repomix-provider.ts
CHANGED
|
@@ -2,18 +2,20 @@ import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pack, setLogLevel } from "repomix";
|
|
5
|
-
import type { SemCandidate, SemChange, SemContext, SemContextPack } from "./types.ts";
|
|
5
|
+
import type { RepomixSearchConfig, SemCandidate, SemChange, SemContext, SemContextPack } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
const MAX_PACK_FILE_SIZE_BYTES = 48 * 1024;
|
|
8
8
|
const MAX_PACK_TOTAL_SIZE_BYTES = 128 * 1024;
|
|
9
9
|
|
|
10
10
|
export function emptyContextPack(): SemContextPack {
|
|
11
|
+
const config = repomixSearchConfig();
|
|
11
12
|
return {
|
|
12
13
|
provider: "repomix",
|
|
13
14
|
filePaths: [],
|
|
14
15
|
totalCharacters: 0,
|
|
15
16
|
totalTokens: 0,
|
|
16
17
|
text: "",
|
|
18
|
+
config,
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -21,10 +23,14 @@ export async function repomixContextPack(
|
|
|
21
23
|
cwd: string,
|
|
22
24
|
contexts: readonly SemContext[],
|
|
23
25
|
changes: readonly SemChange[],
|
|
26
|
+
config = repomixSearchConfig(),
|
|
24
27
|
): Promise<SemContextPack> {
|
|
25
|
-
const filePaths = await candidateFilePaths(cwd, contexts, changes);
|
|
28
|
+
const filePaths = await candidateFilePaths(cwd, contexts, changes, config);
|
|
26
29
|
if (filePaths.length === 0) {
|
|
27
|
-
return
|
|
30
|
+
return {
|
|
31
|
+
...emptyContextPack(),
|
|
32
|
+
config,
|
|
33
|
+
};
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
setLogLevel(-1);
|
|
@@ -35,7 +41,7 @@ export async function repomixContextPack(
|
|
|
35
41
|
[cwd],
|
|
36
42
|
{
|
|
37
43
|
cwd,
|
|
38
|
-
input: { maxFileSize:
|
|
44
|
+
input: { maxFileSize: config.maxFileSizeBytes },
|
|
39
45
|
output: {
|
|
40
46
|
filePath: outputPath,
|
|
41
47
|
style: "xml",
|
|
@@ -44,10 +50,10 @@ export async function repomixContextPack(
|
|
|
44
50
|
directoryStructure: false,
|
|
45
51
|
files: true,
|
|
46
52
|
removeComments: false,
|
|
47
|
-
removeEmptyLines:
|
|
48
|
-
compress:
|
|
53
|
+
removeEmptyLines: config.removeEmptyLines,
|
|
54
|
+
compress: config.compress,
|
|
49
55
|
topFilesLength: 0,
|
|
50
|
-
showLineNumbers:
|
|
56
|
+
showLineNumbers: config.showLineNumbers,
|
|
51
57
|
truncateBase64: true,
|
|
52
58
|
copyToClipboard: false,
|
|
53
59
|
includeFullDirectoryStructure: false,
|
|
@@ -65,7 +71,7 @@ export async function repomixContextPack(
|
|
|
65
71
|
useGitignore: true,
|
|
66
72
|
useDotIgnore: true,
|
|
67
73
|
useDefaultPatterns: true,
|
|
68
|
-
customPatterns: [],
|
|
74
|
+
customPatterns: [...config.ignorePatterns],
|
|
69
75
|
},
|
|
70
76
|
security: { enableSecurityCheck: false },
|
|
71
77
|
tokenCount: { encoding: "o200k_base" },
|
|
@@ -80,6 +86,7 @@ export async function repomixContextPack(
|
|
|
80
86
|
totalCharacters: result.totalCharacters,
|
|
81
87
|
totalTokens: result.totalTokens,
|
|
82
88
|
text: await readFile(outputPath, "utf8"),
|
|
89
|
+
config,
|
|
83
90
|
};
|
|
84
91
|
} finally {
|
|
85
92
|
await rm(tempDir, { recursive: true, force: true });
|
|
@@ -120,6 +127,7 @@ async function candidateFilePaths(
|
|
|
120
127
|
cwd: string,
|
|
121
128
|
contexts: readonly SemContext[],
|
|
122
129
|
changes: readonly SemChange[],
|
|
130
|
+
config: RepomixSearchConfig,
|
|
123
131
|
): Promise<readonly string[]> {
|
|
124
132
|
const byEntityId = new Map(changes.map((change) => [change.entityId, change.filePath]));
|
|
125
133
|
const paths = contexts.flatMap((context) => context.filePath ?? byEntityId.get(context.entityId) ?? []);
|
|
@@ -127,15 +135,63 @@ async function candidateFilePaths(
|
|
|
127
135
|
const selected = [];
|
|
128
136
|
let totalBytes = 0;
|
|
129
137
|
for (const filePath of safePaths) {
|
|
138
|
+
if (matchesAnyPattern(filePath, config.ignorePatterns)) continue;
|
|
130
139
|
const bytes = await fileSize(cwd, filePath);
|
|
131
|
-
if (bytes === null || bytes >
|
|
132
|
-
if (totalBytes + bytes >
|
|
140
|
+
if (bytes === null || bytes > config.maxFileSizeBytes) continue;
|
|
141
|
+
if (totalBytes + bytes > config.maxTotalSizeBytes) continue;
|
|
133
142
|
totalBytes += bytes;
|
|
134
143
|
selected.push(filePath);
|
|
135
144
|
}
|
|
136
145
|
return selected;
|
|
137
146
|
}
|
|
138
147
|
|
|
148
|
+
export function repomixSearchConfig(): RepomixSearchConfig {
|
|
149
|
+
return {
|
|
150
|
+
compress: envBoolean("STUPIFY_REPOMIX_COMPRESS", true),
|
|
151
|
+
showLineNumbers: envBoolean("STUPIFY_REPOMIX_SHOW_LINE_NUMBERS", true),
|
|
152
|
+
removeEmptyLines: envBoolean("STUPIFY_REPOMIX_REMOVE_EMPTY_LINES", true),
|
|
153
|
+
maxFileSizeBytes: envInteger("STUPIFY_REPOMIX_MAX_FILE_BYTES", MAX_PACK_FILE_SIZE_BYTES),
|
|
154
|
+
maxTotalSizeBytes: envInteger("STUPIFY_REPOMIX_MAX_TOTAL_BYTES", MAX_PACK_TOTAL_SIZE_BYTES),
|
|
155
|
+
ignorePatterns: envList("STUPIFY_REPOMIX_IGNORE_PATTERNS"),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function envBoolean(name: string, fallback: boolean): boolean {
|
|
160
|
+
const value = process.env[name];
|
|
161
|
+
if (value === undefined || value === "") return fallback;
|
|
162
|
+
return /^(1|true|yes|on)$/i.test(value);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function envInteger(name: string, fallback: number): number {
|
|
166
|
+
const value = Number(process.env[name]);
|
|
167
|
+
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function envList(name: string): readonly string[] {
|
|
171
|
+
return (process.env[name] ?? "")
|
|
172
|
+
.split(",")
|
|
173
|
+
.map((item) => item.trim())
|
|
174
|
+
.filter(Boolean);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function matchesAnyPattern(filePath: string, patterns: readonly string[]): boolean {
|
|
178
|
+
return patterns.some((pattern) => matchesPattern(filePath, pattern));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function matchesPattern(filePath: string, pattern: string): boolean {
|
|
182
|
+
if (pattern === filePath) return true;
|
|
183
|
+
if (!pattern.includes("*")) return false;
|
|
184
|
+
const escaped = pattern
|
|
185
|
+
.split("*")
|
|
186
|
+
.map(escapeRegExp)
|
|
187
|
+
.join(".*");
|
|
188
|
+
return new RegExp(`^${escaped}$`).test(filePath);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function escapeRegExp(value: string): string {
|
|
192
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
193
|
+
}
|
|
194
|
+
|
|
139
195
|
function isSafeRelativeFilePath(value: string): boolean {
|
|
140
196
|
if (!value || path.isAbsolute(value)) return false;
|
|
141
197
|
const normalized = path.normalize(value);
|