@stupify/cli 0.0.16 → 0.2.0

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 (89) hide show
  1. package/.review/CORPUS.md +44 -0
  2. package/.review/CORPUS.template.md +73 -0
  3. package/.review/REVIEW-PROMPT.md +52 -0
  4. package/.review/RUBRIC.md +46 -0
  5. package/LICENSE +1 -1
  6. package/README.md +95 -37
  7. package/package.json +27 -26
  8. package/packs/antirez.md +10 -0
  9. package/packs/anton-kropp.md +10 -0
  10. package/packs/dhh.md +10 -0
  11. package/packs/dtolnay.md +10 -0
  12. package/packs/jarred-sumner.md +9 -0
  13. package/packs/mitchell-hashimoto.md +10 -0
  14. package/packs/rich-harris.md +10 -0
  15. package/packs/simon-willison.md +10 -0
  16. package/packs/sindre-sorhus.md +10 -0
  17. package/packs/tanner-linsley.md +10 -0
  18. package/packs/zod.md +10 -0
  19. package/src/cli.ts +626 -0
  20. package/src/prime-install.test.ts +109 -0
  21. package/src/prime.ts +50 -0
  22. package/src/review-sweep.test.ts +101 -0
  23. package/src/review-sweep.ts +526 -0
  24. package/dist/analysis.d.ts +0 -16
  25. package/dist/analysis.js +0 -168
  26. package/dist/cache.d.ts +0 -2
  27. package/dist/cache.js +0 -57
  28. package/dist/checks.d.ts +0 -4
  29. package/dist/checks.js +0 -228
  30. package/dist/command.d.ts +0 -2
  31. package/dist/command.js +0 -147
  32. package/dist/constants.d.ts +0 -4
  33. package/dist/constants.js +0 -53
  34. package/dist/counter-scout.d.ts +0 -21
  35. package/dist/counter-scout.js +0 -167
  36. package/dist/diff.d.ts +0 -1
  37. package/dist/diff.js +0 -10
  38. package/dist/doctor.d.ts +0 -16
  39. package/dist/doctor.js +0 -143
  40. package/dist/git.d.ts +0 -17
  41. package/dist/git.js +0 -368
  42. package/dist/hooks.d.ts +0 -5
  43. package/dist/hooks.js +0 -135
  44. package/dist/index.d.ts +0 -1
  45. package/dist/index.js +0 -1
  46. package/dist/model.d.ts +0 -11
  47. package/dist/model.js +0 -296
  48. package/dist/prompts.d.ts +0 -8
  49. package/dist/prompts.js +0 -89
  50. package/dist/render.d.ts +0 -6
  51. package/dist/render.js +0 -295
  52. package/dist/repomix-provider.d.ts +0 -12
  53. package/dist/repomix-provider.js +0 -196
  54. package/dist/search-bench.d.ts +0 -1
  55. package/dist/search-bench.js +0 -677
  56. package/dist/search-profile.d.ts +0 -6
  57. package/dist/search-profile.js +0 -73
  58. package/dist/sem-provider.d.ts +0 -2
  59. package/dist/sem-provider.js +0 -255
  60. package/dist/stupify.d.ts +0 -38
  61. package/dist/stupify.js +0 -505
  62. package/dist/trace.d.ts +0 -31
  63. package/dist/trace.js +0 -86
  64. package/dist/types.d.ts +0 -341
  65. package/dist/types.js +0 -6
  66. package/dist/ui.d.ts +0 -34
  67. package/dist/ui.js +0 -143
  68. package/src/analysis.ts +0 -223
  69. package/src/cache.ts +0 -63
  70. package/src/checks.ts +0 -231
  71. package/src/command.ts +0 -173
  72. package/src/constants.ts +0 -56
  73. package/src/counter-scout.ts +0 -195
  74. package/src/diff.ts +0 -9
  75. package/src/doctor.ts +0 -166
  76. package/src/git.ts +0 -380
  77. package/src/hooks.ts +0 -151
  78. package/src/index.ts +0 -1
  79. package/src/model.ts +0 -367
  80. package/src/prompts.ts +0 -100
  81. package/src/render.ts +0 -328
  82. package/src/repomix-provider.ts +0 -219
  83. package/src/search-bench.ts +0 -783
  84. package/src/search-profile.ts +0 -89
  85. package/src/sem-provider.ts +0 -300
  86. package/src/stupify.ts +0 -604
  87. package/src/trace.ts +0 -126
  88. package/src/types.ts +0 -362
  89. package/src/ui.ts +0 -187
package/src/constants.ts DELETED
@@ -1,56 +0,0 @@
1
- export const VERSION = "0.0.16";
2
- import type { ModelConfig, ModelId } from "./types.ts";
3
-
4
- export const DEFAULT_MODEL_ID: ModelId = "gemma-4-e2b";
5
-
6
- export const MODEL_REGISTRY: Record<ModelId, ModelConfig> = {
7
- "gemma-4-e2b": {
8
- id: "gemma-4-e2b",
9
- name: "Gemma 4 E2B Instruct Q4_K_M",
10
- size: "about 3.1 GB",
11
- file: "gemma-4-e2b-it-q4_k_m.gguf",
12
- url: "https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q4_K_M.gguf?download=true",
13
- },
14
- "gemma-4-e4b": {
15
- id: "gemma-4-e4b",
16
- name: "Gemma 4 E4B Instruct Q4_K_M",
17
- size: "about 5.0 GB",
18
- file: "gemma-4-e4b-it-q4_k_m.gguf",
19
- url: "https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q4_K_M.gguf?download=true",
20
- },
21
- "gemma-4-26b-a4b": {
22
- id: "gemma-4-26b-a4b",
23
- name: "Gemma 4 26B A4B Instruct UD-IQ2_XXS",
24
- size: "about 9.9 GB",
25
- file: "gemma-4-26b-a4b-it-ud-iq2_xxs.gguf",
26
- url: "https://huggingface.co/unsloth/gemma-4-26B-A4B-it-GGUF/resolve/main/gemma-4-26B-A4B-it-UD-IQ2_XXS.gguf?download=true",
27
- },
28
- "qwen3-4b-magicquant": {
29
- id: "qwen3-4b-magicquant",
30
- name: "Qwen3-4B-Instruct-2507 MagicQuant Q4_K_M",
31
- size: "about 2.4 GB",
32
- file: "qwen3-4b-instruct-2507-magicquant-q4_k_m.gguf",
33
- url: "https://huggingface.co/magiccodingman/Qwen3-4B-Instruct-2507-Unsloth-MagicQuant-v2-GGUF/resolve/main/Model-MQ-Q4_K_M_1.gguf?download=true",
34
- },
35
- "qwen2.5-coder-1.5b": {
36
- id: "qwen2.5-coder-1.5b",
37
- name: "Qwen2.5-Coder-1.5B-Instruct Q4_K_M",
38
- size: "about 1.1 GB",
39
- file: "qwen2.5-coder-1.5b-instruct-q4_k_m.gguf",
40
- url: "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf?download=true",
41
- },
42
- "qwen2.5-coder-7b": {
43
- id: "qwen2.5-coder-7b",
44
- name: "Qwen2.5-Coder-7B-Instruct Q4_K_M",
45
- size: "about 4.7 GB",
46
- file: "qwen2.5-coder-7b-instruct-q4_k_m.gguf",
47
- url: "https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-GGUF/resolve/main/qwen2.5-coder-7b-instruct-q4_k_m.gguf?download=true",
48
- },
49
- "qwen2.5-coder-32b": {
50
- id: "qwen2.5-coder-32b",
51
- name: "Qwen2.5-Coder-32B-Instruct Q4_K_M",
52
- size: "about 19 GB",
53
- file: "qwen2.5-coder-32b-instruct-q4_k_m.gguf",
54
- url: "https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct-GGUF/resolve/main/qwen2.5-coder-32b-instruct-q4_k_m.gguf?download=true",
55
- },
56
- };
@@ -1,195 +0,0 @@
1
- import type { CheckId, SemCandidate, SemChange, SemChangeSet, StupifyCheck } from "./types.ts";
2
-
3
- type Signal = Readonly<{
4
- checkId: CheckId;
5
- entityId: string;
6
- reasonCode: string;
7
- }>;
8
-
9
- type SignalBucket = Readonly<{
10
- checkId: CheckId;
11
- total: number;
12
- examples: readonly Signal[];
13
- }>;
14
-
15
- const MAX_COUNTER_EXAMPLES_PER_CHECK = 20;
16
-
17
- export type CounterScoutPlan = Readonly<{
18
- buckets: readonly SignalBucket[];
19
- totalSignals: number;
20
- maxTargets: number;
21
- targets: readonly SemCandidate[];
22
- }>;
23
-
24
- export function counterScoutTargets(
25
- changeSet: SemChangeSet,
26
- checks: readonly StupifyCheck[],
27
- maxTargets: number,
28
- ): readonly SemCandidate[] {
29
- return counterScoutPlan(changeSet, checks, maxTargets).targets;
30
- }
31
-
32
- export function counterScoutPlan(
33
- changeSet: SemChangeSet,
34
- checks: readonly StupifyCheck[],
35
- maxTargets: number,
36
- ): CounterScoutPlan {
37
- const buckets = runSignalCounters(changeSet, checks);
38
- const targets: SemCandidate[] = [];
39
- let cursor = 0;
40
- while (targets.length < maxTargets && buckets.some((bucket) => cursor < bucket.examples.length)) {
41
- for (const bucket of buckets) {
42
- const signal = bucket.examples[cursor];
43
- if (!signal) continue;
44
- targets.push({
45
- sourceId: changeSet.id,
46
- targetId: `t${String(targets.length + 1).padStart(3, "0")}`,
47
- entityId: signal.entityId,
48
- checkId: signal.checkId,
49
- reason: signal.reasonCode,
50
- });
51
- if (targets.length >= maxTargets) break;
52
- }
53
- cursor += 1;
54
- }
55
- return {
56
- buckets,
57
- totalSignals: buckets.reduce((sum, bucket) => sum + bucket.total, 0),
58
- maxTargets,
59
- targets,
60
- };
61
- }
62
-
63
- export function runSignalCounters(
64
- changeSet: SemChangeSet,
65
- checks: readonly StupifyCheck[],
66
- ): readonly SignalBucket[] {
67
- return checks
68
- .map((check) => {
69
- const signals = changeSet.changes.flatMap((change): readonly Signal[] => {
70
- const reasonCode = reasonForCheck(check.id, change);
71
- return reasonCode ? [{ checkId: check.id, entityId: change.entityId, reasonCode }] : [];
72
- });
73
- return {
74
- checkId: check.id,
75
- total: signals.length,
76
- examples: signals.slice(0, MAX_COUNTER_EXAMPLES_PER_CHECK),
77
- };
78
- })
79
- .filter((bucket) => bucket.total > 0);
80
- }
81
-
82
- function reasonForCheck(checkId: CheckId, change: SemChange): string | null {
83
- if (!isSearchableSourceChange(change)) return null;
84
-
85
- const haystack = `${change.entityName}\n${change.entityType}\n${change.filePath}\n${change.afterContent ?? ""}`.toLowerCase();
86
- const changed = change.changeType === "added" || change.changeType === "modified";
87
- if (!changed) return null;
88
-
89
- switch (checkId as string) {
90
- case "duplicated_schema":
91
- return isDuplicatedSchemaCandidate(change) ? "local_schemaish_copy" : null;
92
- case "unnecessary_complexity":
93
- return /\b(helper|wrapper|service|provider|manager|factory|adapter|resolver|coordinator)\b/i.test(change.entityName)
94
- ? "new_abstraction_name"
95
- : null;
96
- case "fake_precision_windowing":
97
- return /\b(token|budget|window|batch|ratio|estimate|counter|count|limit)\b/i.test(haystack)
98
- ? "precision_accounting_terms"
99
- : null;
100
- case "coauthored_slop":
101
- return /\b(coauhtoried|coauthored|co-authored|co-authored-by)\b/i.test(haystack)
102
- ? "coauthor_text"
103
- : null;
104
- case "mega_file":
105
- return change.entityType === "chunk" && /lines\s+\d+-\d+/i.test(change.entityName)
106
- ? "large_changed_chunk"
107
- : null;
108
- case "over_commenting":
109
- return overCommentingSignal(change)
110
- ? "comment_lines_increased"
111
- : null;
112
- case "lint_bypass":
113
- return lintBypassSignal(change.afterContent ?? "")
114
- ? "lint_or_type_bypass_text"
115
- : null;
116
- case "inconsistent_patterns":
117
- return /\b(manager|factory|provider|adapter|orchestrator|coordinator)\b/i.test(change.entityName)
118
- ? "pattern_abstraction_name"
119
- : null;
120
- case "reinvented_utils":
121
- return reinventedUtilitySignal(change)
122
- ? "generic_utility_name"
123
- : null;
124
- case "operator_style_mismatch":
125
- return /\b(manager|factory|provider|enterprise|orchestrator)\b/i.test(haystack)
126
- ? "style_smell_terms"
127
- : null;
128
- default:
129
- return null;
130
- }
131
- }
132
-
133
- function isDuplicatedSchemaCandidate(change: SemChange): boolean {
134
- if (!/^(interface|type)$/i.test(change.entityType)) return false;
135
- if (/^(public|external|internal|payment|.+client$)/i.test(change.entityName)) return false;
136
- return /\b(local|payload|schema)\b/i.test(words(change.entityName));
137
- }
138
-
139
- function overCommentingSignal(change: SemChange): boolean {
140
- const before = commentLines(change.beforeContent);
141
- const after = commentLines(change.afterContent);
142
- if (after <= before + 3) return false;
143
- const comments = commentText(change.afterContent);
144
- if (/\b(because|why|constraint|provider|external|api|quirk|edge case|timezone|utc|ledger|finance|reconciliation|rejects|mirrors|keep this)\b/i.test(comments)) {
145
- return false;
146
- }
147
- return true;
148
- }
149
-
150
- function lintBypassSignal(value: string): boolean {
151
- return value.split(/\r?\n/).some((line) => {
152
- const trimmed = line.trim();
153
- const comment = /^(\/\/|\/\*|\*)/.test(trimmed);
154
- if (comment && /@ts-ignore\s*$/i.test(trimmed)) return true;
155
- if (comment && /@ts-expect-error\s*$/i.test(trimmed)) return true;
156
- if (comment && /(eslint-disable|biome-ignore)/i.test(trimmed) && !/\s--\s*\S/.test(trimmed)) return true;
157
- return /\bas unknown as\b|\bas any\b|:\s*any\b/i.test(trimmed);
158
- });
159
- }
160
-
161
- function reinventedUtilitySignal(change: SemChange): boolean {
162
- const name = change.entityName;
163
- if (!/^(clamp|debounce|throttle|slug|slugify|sort|shuffle|memoize|pick|omit|uniq)/i.test(name)) return false;
164
- const content = change.afterContent ?? "";
165
- if (/currency|invoice|refund|subscription|tier|domain/i.test(`${name}\n${content}`)) return false;
166
- return true;
167
- }
168
-
169
- function isSearchableSourceChange(change: SemChange): boolean {
170
- const filePath = change.filePath.toLowerCase();
171
- if (/(^|\/)(bun|package-lock|pnpm-lock|yarn)\.lock$/.test(filePath)) return false;
172
- if (/(^|\/)(dist|build|coverage|generated|vendor|fixtures?|snapshots?)(\/|$)/.test(filePath)) return false;
173
- if (/\.(md|mdx|txt|json|jsonc|ya?ml|toml|lock|csv|svg|png|jpe?g|gif|webp)$/i.test(filePath)) return false;
174
- if (/\.(test|spec|fixture)\.[cm]?[jt]sx?$/i.test(filePath)) return false;
175
- return /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/i.test(filePath);
176
- }
177
-
178
- function commentLines(value: string | null): number {
179
- if (!value) return 0;
180
- return value.split(/\r?\n/).filter((line) => /^\s*(\/\/|\/\*|\*|#)/.test(line)).length;
181
- }
182
-
183
- function commentText(value: string | null): string {
184
- if (!value) return "";
185
- return value
186
- .split(/\r?\n/)
187
- .filter((line) => /^\s*(\/\/|\/\*|\*|#)/.test(line))
188
- .join("\n");
189
- }
190
-
191
- function words(value: string): string {
192
- return value
193
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
194
- .replace(/[_-]+/g, " ");
195
- }
package/src/diff.ts DELETED
@@ -1,9 +0,0 @@
1
- import { stdin as input } from "node:process";
2
-
3
- export async function readDiffFromStdin(): Promise<string> {
4
- const chunks: Buffer[] = [];
5
- for await (const chunk of input) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
6
- const text = Buffer.concat(chunks).toString("utf8");
7
- if (!text.trim()) throw new Error("No diff received on stdin.");
8
- return text;
9
- }
package/src/doctor.ts DELETED
@@ -1,166 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { createRequire } from "node:module";
3
- import { homedir, platform } from "node:os";
4
- import path from "node:path";
5
- import { promisify } from "node:util";
6
- import { DEFAULT_MODEL_ID, MODEL_REGISTRY } from "./constants.ts";
7
- import { runHookCommand } from "./hooks.ts";
8
- import type { CliUi } from "./ui.ts";
9
-
10
- const execFileAsync = promisify(execFile);
11
-
12
- type CheckStatus = "ok" | "missing" | "info";
13
-
14
- type DoctorCheck = Readonly<{
15
- label: string;
16
- status: CheckStatus;
17
- detail: string;
18
- required?: boolean;
19
- }>;
20
-
21
- export type DoctorResult = Readonly<{
22
- exitCode: number;
23
- text: string;
24
- checks: readonly DoctorCheck[];
25
- }>;
26
-
27
- export async function runDoctor(): Promise<DoctorResult> {
28
- const checks = await Promise.all([
29
- gitCheck(),
30
- hookCheck(),
31
- semCheck(),
32
- repomixCheck(),
33
- llamaServerCheck(),
34
- modelCacheCheck(),
35
- ]);
36
- const requiredMissing = checks.some((check) => check.required && check.status === "missing");
37
- return {
38
- exitCode: requiredMissing ? 1 : 0,
39
- text: renderDoctor(checks),
40
- checks,
41
- };
42
- }
43
-
44
- export function renderDoctorToUi(result: DoctorResult, ui: CliUi): void {
45
- const missingRequired = result.checks.filter((check) => check.required && check.status === "missing");
46
- if (missingRequired.length > 0) {
47
- ui.error(`Doctor found ${missingRequired.length} missing required dependency.`);
48
- } else {
49
- ui.success("Doctor checks complete.");
50
- }
51
-
52
- ui.note(
53
- result.checks.map((check) => `${icon(check.status)} ${check.label}: ${check.detail}`).join("\n"),
54
- "Doctor",
55
- );
56
- ui.note(
57
- "Local-only. Stupify does not upload source, diffs, filenames, repo URLs, commit messages, author names, or private package names.",
58
- "Privacy",
59
- );
60
- }
61
-
62
- async function gitCheck(): Promise<DoctorCheck> {
63
- try {
64
- const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { maxBuffer: 1024 * 1024 });
65
- return { label: "git repo", status: "ok", detail: stdout.trim(), required: true };
66
- } catch {
67
- return { label: "git repo", status: "missing", detail: "not inside a git repository", required: true };
68
- }
69
- }
70
-
71
- async function hookCheck(): Promise<DoctorCheck> {
72
- try {
73
- const status = await runHookCommand("status");
74
- return { label: "pre-commit hook", status: "info", detail: status.replace(/^Stupify hook:\s*/, "") };
75
- } catch (error) {
76
- return { label: "pre-commit hook", status: "info", detail: errorMessage(error) };
77
- }
78
- }
79
-
80
- async function semCheck(): Promise<DoctorCheck> {
81
- const packageBin = resolvePackage("@ataraxy-labs/sem/bin/sem.js");
82
- if (packageBin) return { label: "sem", status: "ok", detail: "@ataraxy-labs/sem package binary found", required: true };
83
- if (await commandExists("sem")) return { label: "sem", status: "ok", detail: "sem found on PATH", required: true };
84
- return { label: "sem", status: "missing", detail: "install @ataraxy-labs/sem or put sem on PATH", required: true };
85
- }
86
-
87
- async function repomixCheck(): Promise<DoctorCheck> {
88
- if (resolvePackage("repomix")) return { label: "Repomix", status: "ok", detail: "repomix package found", required: true };
89
- return { label: "Repomix", status: "missing", detail: "repomix package is not installed", required: true };
90
- }
91
-
92
- async function llamaServerCheck(): Promise<DoctorCheck> {
93
- if (await commandExists("llama-server")) return { label: "llama-server", status: "ok", detail: "llama-server found on PATH", required: true };
94
- return { label: "llama-server", status: "missing", detail: "install llama.cpp, for example `brew install llama.cpp`", required: true };
95
- }
96
-
97
- async function modelCacheCheck(): Promise<DoctorCheck> {
98
- const model = MODEL_REGISTRY[DEFAULT_MODEL_ID];
99
- const modelPath = path.join(cacheDir(), "models", model.file);
100
- if (await fileExists(modelPath)) return { label: "default model", status: "ok", detail: `${model.name} cached` };
101
- return {
102
- label: "default model",
103
- status: "info",
104
- detail: `${model.name} not cached yet; first interactive search can download it locally`,
105
- };
106
- }
107
-
108
- function renderDoctor(checks: readonly DoctorCheck[]): string {
109
- const lines = [
110
- "Stupify doctor",
111
- "",
112
- ...checks.map((check) => `${icon(check.status)} ${check.label}: ${check.detail}`),
113
- "",
114
- "Privacy: local-only. Stupify does not upload source, diffs, filenames, repo URLs, commit messages, author names, or private package names.",
115
- ];
116
- return lines.join("\n");
117
- }
118
-
119
- function icon(status: CheckStatus): string {
120
- if (status === "ok") return "OK";
121
- if (status === "missing") return "MISSING";
122
- return "INFO";
123
- }
124
-
125
- function resolvePackage(specifier: string): string | null {
126
- try {
127
- const require = createRequire(import.meta.url);
128
- return require.resolve(specifier);
129
- } catch {
130
- return null;
131
- }
132
- }
133
-
134
- async function commandExists(command: string): Promise<boolean> {
135
- try {
136
- await execFileAsync("sh", ["-c", `command -v ${shellQuote(command)}`], { maxBuffer: 1024 * 1024 });
137
- return true;
138
- } catch {
139
- return false;
140
- }
141
- }
142
-
143
- async function fileExists(filePath: string): Promise<boolean> {
144
- try {
145
- const { stat } = await import("node:fs/promises");
146
- return (await stat(filePath)).isFile();
147
- } catch {
148
- return false;
149
- }
150
- }
151
-
152
- function cacheDir(): string {
153
- if (process.env.STUPIFY_CACHE_DIR) return process.env.STUPIFY_CACHE_DIR;
154
- if (process.env.XDG_CACHE_HOME) return path.join(process.env.XDG_CACHE_HOME, "stupify");
155
- if (platform() === "darwin") return path.join(homedir(), "Library", "Caches", "stupify");
156
- if (platform() === "win32" && process.env.LOCALAPPDATA) return path.join(process.env.LOCALAPPDATA, "stupify", "Cache");
157
- return path.join(homedir(), ".cache", "stupify");
158
- }
159
-
160
- function shellQuote(value: string): string {
161
- return `'${value.replace(/'/g, "'\\''")}'`;
162
- }
163
-
164
- function errorMessage(error: unknown): string {
165
- return error instanceof Error ? error.message : String(error);
166
- }