@stupify/cli 0.0.16 → 0.1.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 (74) hide show
  1. package/.review/CORPUS.md +73 -0
  2. package/.review/REVIEW-PROMPT.md +52 -0
  3. package/.review/RUBRIC.md +46 -0
  4. package/LICENSE +1 -1
  5. package/README.md +41 -39
  6. package/package.json +24 -25
  7. package/src/cli.ts +358 -0
  8. package/src/review-sweep.ts +492 -0
  9. package/dist/analysis.d.ts +0 -16
  10. package/dist/analysis.js +0 -168
  11. package/dist/cache.d.ts +0 -2
  12. package/dist/cache.js +0 -57
  13. package/dist/checks.d.ts +0 -4
  14. package/dist/checks.js +0 -228
  15. package/dist/command.d.ts +0 -2
  16. package/dist/command.js +0 -147
  17. package/dist/constants.d.ts +0 -4
  18. package/dist/constants.js +0 -53
  19. package/dist/counter-scout.d.ts +0 -21
  20. package/dist/counter-scout.js +0 -167
  21. package/dist/diff.d.ts +0 -1
  22. package/dist/diff.js +0 -10
  23. package/dist/doctor.d.ts +0 -16
  24. package/dist/doctor.js +0 -143
  25. package/dist/git.d.ts +0 -17
  26. package/dist/git.js +0 -368
  27. package/dist/hooks.d.ts +0 -5
  28. package/dist/hooks.js +0 -135
  29. package/dist/index.d.ts +0 -1
  30. package/dist/index.js +0 -1
  31. package/dist/model.d.ts +0 -11
  32. package/dist/model.js +0 -296
  33. package/dist/prompts.d.ts +0 -8
  34. package/dist/prompts.js +0 -89
  35. package/dist/render.d.ts +0 -6
  36. package/dist/render.js +0 -295
  37. package/dist/repomix-provider.d.ts +0 -12
  38. package/dist/repomix-provider.js +0 -196
  39. package/dist/search-bench.d.ts +0 -1
  40. package/dist/search-bench.js +0 -677
  41. package/dist/search-profile.d.ts +0 -6
  42. package/dist/search-profile.js +0 -73
  43. package/dist/sem-provider.d.ts +0 -2
  44. package/dist/sem-provider.js +0 -255
  45. package/dist/stupify.d.ts +0 -38
  46. package/dist/stupify.js +0 -505
  47. package/dist/trace.d.ts +0 -31
  48. package/dist/trace.js +0 -86
  49. package/dist/types.d.ts +0 -341
  50. package/dist/types.js +0 -6
  51. package/dist/ui.d.ts +0 -34
  52. package/dist/ui.js +0 -143
  53. package/src/analysis.ts +0 -223
  54. package/src/cache.ts +0 -63
  55. package/src/checks.ts +0 -231
  56. package/src/command.ts +0 -173
  57. package/src/constants.ts +0 -56
  58. package/src/counter-scout.ts +0 -195
  59. package/src/diff.ts +0 -9
  60. package/src/doctor.ts +0 -166
  61. package/src/git.ts +0 -380
  62. package/src/hooks.ts +0 -151
  63. package/src/index.ts +0 -1
  64. package/src/model.ts +0 -367
  65. package/src/prompts.ts +0 -100
  66. package/src/render.ts +0 -328
  67. package/src/repomix-provider.ts +0 -219
  68. package/src/search-bench.ts +0 -783
  69. package/src/search-profile.ts +0 -89
  70. package/src/sem-provider.ts +0 -300
  71. package/src/stupify.ts +0 -604
  72. package/src/trace.ts +0 -126
  73. package/src/types.ts +0 -362
  74. package/src/ui.ts +0 -187
package/src/model.ts DELETED
@@ -1,367 +0,0 @@
1
- import { execFile, spawn } from "node:child_process";
2
- import {
3
- mkdir,
4
- open,
5
- readFile,
6
- rename,
7
- rm,
8
- stat,
9
- writeFile,
10
- } from "node:fs/promises";
11
- import { homedir, platform } from "node:os";
12
- import path from "node:path";
13
- import { promisify } from "node:util";
14
- import { MODEL_REGISTRY } from "./constants.ts";
15
- import type { ModelId } from "./types.ts";
16
- import type { CliUi } from "./ui.ts";
17
-
18
- const execFileAsync = promisify(execFile);
19
- const LLAMA_SERVER_HOST = "127.0.0.1";
20
-
21
- export type ModelProfile = "scout";
22
-
23
- type ModelRuntime = Readonly<{
24
- profile: ModelProfile;
25
- baseUrl: string;
26
- port: string;
27
- contextSize: number;
28
- reasoning: "on" | "off" | "auto";
29
- reasoningBudget?: number;
30
- gpuLayers?: number;
31
- batchSize?: number;
32
- ubatchSize?: number;
33
- parallel?: number;
34
- threads?: number;
35
- threadsBatch?: number;
36
- flashAttention?: boolean;
37
- }>;
38
-
39
- export type LocalModel = Readonly<{
40
- id: ModelId;
41
- name: string;
42
- baseUrl: string;
43
- profile: ModelProfile;
44
- }>;
45
-
46
- export async function firstRunModelBootstrap(
47
- modelId: ModelId,
48
- ui: CliUi,
49
- ): Promise<string> {
50
- const selectedModel = MODEL_REGISTRY[modelId];
51
- const modelDir = path.join(cacheDir(), "models");
52
- const modelPath = path.join(modelDir, selectedModel.file);
53
- if (await exists(modelPath)) return modelPath;
54
-
55
- ui.note(`No local Stupify model found.
56
- Stupify runs locally.
57
- Download this model now?
58
- Model: ${selectedModel.name}
59
- Size: ${selectedModel.size}`, "Setup", { force: true });
60
-
61
- if (!(await ui.confirm("Continue?"))) throw new Error("Setup cancelled.");
62
-
63
- await mkdir(modelDir, { recursive: true });
64
- await downloadModel(modelPath, selectedModel.url, ui);
65
- if (!(await exists(modelPath)))
66
- throw new Error("Model download failed: file was not created.");
67
- return modelPath;
68
- }
69
-
70
- export async function loadLocalModel(
71
- modelPath: string,
72
- modelId: ModelId,
73
- profile: ModelProfile,
74
- ui: CliUi,
75
- ): Promise<LocalModel> {
76
- const selectedModel = MODEL_REGISTRY[modelId];
77
- const runtime = modelRuntime(profile);
78
- const runningModel = await runningServerModel(runtime.baseUrl);
79
-
80
- if (runningModel) {
81
- if (runningModel !== modelId) await stopManagedServer(runtime, ui);
82
- if (runningModel === modelId) {
83
- ui.info(`Using local model: ${selectedModel.name}`);
84
- return {
85
- id: modelId,
86
- name: selectedModel.name,
87
- baseUrl: runtime.baseUrl,
88
- profile,
89
- };
90
- }
91
- }
92
-
93
- await ensureLlamaServerBinary();
94
- await startLlamaServer(modelPath, modelId, selectedModel.name, runtime, ui);
95
- const ready = ui.spinner(`Waiting for local ${profile} model`);
96
- try {
97
- await waitForServer(runtime.baseUrl, modelId);
98
- ready.stop(`Local ${profile} model ready`);
99
- } catch (error) {
100
- ready.error(`Local ${profile} model failed to start`);
101
- throw error;
102
- }
103
- return {
104
- id: modelId,
105
- name: selectedModel.name,
106
- baseUrl: runtime.baseUrl,
107
- profile,
108
- };
109
- }
110
-
111
- function modelRuntime(profile: ModelProfile): ModelRuntime {
112
- const baseUrl =
113
- process.env.STUPIFY_SCOUT_LLAMA_SERVER_URL ??
114
- process.env.STUPIFY_LLAMA_SERVER_URL ??
115
- "http://127.0.0.1:8091";
116
- return {
117
- profile,
118
- baseUrl,
119
- port: new URL(baseUrl).port || "8091",
120
- contextSize: envInteger("STUPIFY_LLAMA_CONTEXT") ?? 65_536,
121
- reasoning: "off",
122
- gpuLayers: envInteger("STUPIFY_LLAMA_GPU_LAYERS") ?? 999,
123
- batchSize: envInteger("STUPIFY_LLAMA_BATCH") ?? 2_048,
124
- ubatchSize: envInteger("STUPIFY_LLAMA_UBATCH") ?? 512,
125
- parallel: envInteger("STUPIFY_LLAMA_PARALLEL") ?? 2,
126
- threads: envInteger("STUPIFY_LLAMA_THREADS"),
127
- threadsBatch: envInteger("STUPIFY_LLAMA_THREADS_BATCH"),
128
- flashAttention: envBoolean("STUPIFY_LLAMA_FLASH_ATTN"),
129
- };
130
- }
131
-
132
- async function runningServerModel(baseUrl: string): Promise<string | null> {
133
- try {
134
- const response = await fetch(`${baseUrl}/v1/models`, {
135
- signal: AbortSignal.timeout(500),
136
- });
137
- if (!response.ok) return null;
138
- const body = (await response.json()) as { data?: Array<{ id?: unknown }> };
139
- const id = body.data?.[0]?.id;
140
- return typeof id === "string" ? id : null;
141
- } catch {
142
- return null;
143
- }
144
- }
145
-
146
- async function ensureLlamaServerBinary(): Promise<void> {
147
- try {
148
- await execFileAsync("llama-server", ["--version"], {
149
- maxBuffer: 1024 * 1024,
150
- });
151
- } catch {
152
- throw new Error(`Stupify needs llama-server for local inference.
153
- Install llama.cpp first:
154
- brew install llama.cpp`);
155
- }
156
- }
157
-
158
- async function startLlamaServer(
159
- modelPath: string,
160
- modelId: ModelId,
161
- modelName: string,
162
- runtime: ModelRuntime,
163
- ui: CliUi,
164
- ): Promise<void> {
165
- const logDir = path.join(cacheDir(), "logs");
166
- await mkdir(logDir, { recursive: true });
167
- const logPath = path.join(logDir, "llama-server.log");
168
- const out = await open(logPath, "a");
169
- const err = await open(logPath, "a");
170
-
171
- ui.step(`Starting local model server: ${modelName}`);
172
- ui.info(`llama-server log: ${logPath}`);
173
-
174
- const args = [
175
- "-m",
176
- modelPath,
177
- "-a",
178
- modelId,
179
- "--host",
180
- LLAMA_SERVER_HOST,
181
- "--port",
182
- runtime.port,
183
- "-c",
184
- String(runtime.contextSize),
185
- "--reasoning",
186
- runtime.reasoning,
187
- "--no-warmup",
188
- ];
189
- if (runtime.gpuLayers !== undefined) args.push("-ngl", String(runtime.gpuLayers));
190
- if (runtime.batchSize !== undefined) args.push("-b", String(runtime.batchSize));
191
- if (runtime.ubatchSize !== undefined) args.push("-ub", String(runtime.ubatchSize));
192
- if (runtime.parallel !== undefined) args.push("-np", String(runtime.parallel));
193
- if (runtime.threads !== undefined) args.push("-t", String(runtime.threads));
194
- if (runtime.threadsBatch !== undefined) args.push("-tb", String(runtime.threadsBatch));
195
- if (runtime.flashAttention !== undefined) args.push("-fa", runtime.flashAttention ? "on" : "off");
196
- if (runtime.reasoningBudget !== undefined) {
197
- args.push("--reasoning-budget", String(runtime.reasoningBudget));
198
- }
199
-
200
- const child = spawn("llama-server", args, {
201
- detached: true,
202
- stdio: ["ignore", out.fd, err.fd],
203
- });
204
-
205
- child.unref();
206
- if (child.pid) await writeFile(pidPath(runtime), String(child.pid));
207
- await out.close();
208
- await err.close();
209
- }
210
-
211
- async function stopManagedServer(runtime: ModelRuntime, ui: CliUi): Promise<void> {
212
- const pid = await managedServerPid(runtime);
213
- if (!pid) {
214
- const runningModel = await runningServerModel(runtime.baseUrl);
215
- throw new Error(`A llama-server is already running with ${runningModel ?? "another model"}.
216
- Stop it before switching models, or use STUPIFY_LLAMA_SERVER_URL for that server.`);
217
- }
218
-
219
- ui.step("Restarting local model server for selected model.");
220
- try {
221
- process.kill(pid, "SIGTERM");
222
- } catch {
223
- await rm(pidPath(runtime), { force: true });
224
- return;
225
- }
226
-
227
- const deadline = Date.now() + 15_000;
228
- while (Date.now() < deadline) {
229
- if (!(await runningServerModel(runtime.baseUrl))) {
230
- await rm(pidPath(runtime), { force: true });
231
- return;
232
- }
233
- await sleep(250);
234
- }
235
-
236
- throw new Error("Timed out while stopping existing llama-server.");
237
- }
238
-
239
- async function managedServerPid(runtime: ModelRuntime): Promise<number | null> {
240
- try {
241
- const value = Number((await readFile(pidPath(runtime), "utf8")).trim());
242
- return Number.isInteger(value) && value > 0 ? value : null;
243
- } catch {
244
- return null;
245
- }
246
- }
247
-
248
- function pidPath(_runtime: ModelRuntime): string {
249
- return path.join(cacheDir(), "llama-server.pid");
250
- }
251
-
252
- function envInteger(name: string, fallback?: number): number | undefined {
253
- const raw = process.env[name];
254
- if (raw === undefined || raw === "") return fallback;
255
- const value = Number(raw);
256
- return Number.isInteger(value) && value > 0 ? value : fallback;
257
- }
258
-
259
- function envBoolean(name: string): boolean | undefined {
260
- const raw = process.env[name];
261
- if (raw === undefined || raw === "") return undefined;
262
- return /^(1|true|yes|on)$/i.test(raw);
263
- }
264
-
265
- async function waitForServer(baseUrl: string, modelId: ModelId): Promise<void> {
266
- const deadline = Date.now() + 120_000;
267
- while (Date.now() < deadline) {
268
- const runningModel = await runningServerModel(baseUrl);
269
- if (runningModel === modelId) return;
270
- await sleep(500);
271
- }
272
- throw new Error(`llama-server did not become ready for ${modelId}.`);
273
- }
274
-
275
- function sleep(ms: number): Promise<void> {
276
- return new Promise((resolve) => setTimeout(resolve, ms));
277
- }
278
-
279
- async function downloadModel(
280
- modelPath: string,
281
- modelUrl: string,
282
- ui: CliUi,
283
- ): Promise<void> {
284
- const tempPath = `${modelPath}.download`;
285
- await rm(tempPath, { force: true });
286
-
287
- const downloadSpinner = ui.spinner("Downloading model");
288
- let downloadProgress: ReturnType<CliUi["progress"]> | null = null;
289
- try {
290
- const response = await fetch(modelUrl);
291
- if (!response.ok || !response.body)
292
- throw new Error(`Model download failed: HTTP ${response.status}`);
293
-
294
- const total = Number(response.headers.get("content-length") ?? 0);
295
- let received = 0;
296
- let lastPrint = 0;
297
- let lastProgressBytes = 0;
298
- const reader = response.body.getReader();
299
- const file = await open(tempPath, "wx");
300
- if (total > 0) {
301
- downloadSpinner.clear();
302
- downloadProgress = ui.progress("Downloading model", total);
303
- }
304
-
305
- try {
306
- while (true) {
307
- const { done, value } = await reader.read();
308
- if (done) break;
309
- received += value.byteLength;
310
- await file.write(value);
311
- const now = Date.now();
312
- if (total > 0 && now - lastPrint > 500) {
313
- lastPrint = now;
314
- downloadProgress?.advance(
315
- received - lastProgressBytes,
316
- `${formatBytes(received)} / ${formatBytes(total)}`,
317
- );
318
- lastProgressBytes = received;
319
- }
320
- }
321
- } finally {
322
- await file.close();
323
- }
324
-
325
- if (downloadProgress && received > lastProgressBytes) {
326
- downloadProgress.advance(
327
- received - lastProgressBytes,
328
- `${formatBytes(received)} / ${formatBytes(total)}`,
329
- );
330
- }
331
- const activeProgress = downloadProgress ?? downloadSpinner;
332
- activeProgress.stop(
333
- total > 0
334
- ? `Downloaded ${formatBytes(received)} / ${formatBytes(total)}`
335
- : "Downloaded model",
336
- );
337
- await rename(tempPath, modelPath);
338
- } catch (error) {
339
- const activeProgress = downloadProgress ?? downloadSpinner;
340
- activeProgress.error("Model download failed");
341
- await rm(tempPath, { force: true });
342
- throw error;
343
- }
344
- }
345
-
346
- function cacheDir(): string {
347
- if (process.env.STUPIFY_CACHE_DIR) return process.env.STUPIFY_CACHE_DIR;
348
- if (process.env.XDG_CACHE_HOME)
349
- return path.join(process.env.XDG_CACHE_HOME, "stupify");
350
- if (platform() === "darwin")
351
- return path.join(homedir(), "Library", "Caches", "stupify");
352
- if (platform() === "win32" && process.env.LOCALAPPDATA)
353
- return path.join(process.env.LOCALAPPDATA, "stupify", "Cache");
354
- return path.join(homedir(), ".cache", "stupify");
355
- }
356
-
357
- async function exists(filePath: string): Promise<boolean> {
358
- try {
359
- return (await stat(filePath)).isFile();
360
- } catch {
361
- return false;
362
- }
363
- }
364
-
365
- function formatBytes(bytes: number): string {
366
- return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
367
- }
package/src/prompts.ts DELETED
@@ -1,100 +0,0 @@
1
- import type { SemChangeSet, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
2
-
3
- export function searchPrompt(input: Readonly<{
4
- changeSet: SemChangeSet;
5
- contexts: readonly SemContext[];
6
- pack: SemContextPack;
7
- patterns: readonly StupifyCheck[];
8
- includeCounterReason: boolean;
9
- }>): string {
10
- return `You are Stupify's local search model.
11
- Stupify checks whether AI-assisted coding may be replacing developer judgment.
12
- You will receive:
13
- 1. Semantic changed entities selected by a fast local counter.
14
- 2. Compressed local file context from Repomix.
15
- 3. A list of search targets. Each target has exactly one assigned pattern.
16
-
17
- Your job:
18
- Evaluate each target only against its assigned pattern.
19
- False positives are expensive.
20
- Only emit a match if the assigned pattern clearly applies to that exact target.
21
- Do not perform general code review.
22
- Do not suggest improvements.
23
- Do not choose a pattern.
24
- Do not apply other patterns.
25
- Do not report issues for unlisted targets.
26
- Do not emit clean results.
27
- Omitted target = clean.
28
- Return JSON only:
29
- {
30
- "matches": [
31
- {
32
- "targetId": "t001",
33
- "reason": "one sentence",
34
- "proof": "short pointer"
35
- }
36
- ]
37
- }
38
-
39
- Rules:
40
- - Use only targetIds from the input.
41
- - Emit at most 5 matches.
42
- - Prefer omission over a weak match.
43
- - Do not quote source code.
44
- - Do not write generic feedback.
45
- - Do not emit "no evidence" or "does not apply."
46
- - Proof must point to concrete changed product code that implements the pattern.
47
- - Proof must not be a file header or start with "diff --git".
48
- - Do not use pattern registry text, prompt text, docs, tests, or examples as proof.
49
- - Do not treat pattern or prompt wording as the code being evaluated.
50
- - Do not treat plain conditionals, guard clauses, skip paths, or error handling as indirection.
51
- - For unnecessary_complexity, identify the exact new named abstraction in proof.
52
- - If unnecessary_complexity proof would only be a file, hunk, or conditional block, omit it.
53
- - If nothing clearly matches, return { "matches": [] }.
54
-
55
- SOURCE:
56
- ${input.changeSet.label}
57
-
58
- SEARCH TARGETS:
59
- ${input.contexts.map((context) => formatSearchTarget(context, patternForContext(context, input.patterns), input.includeCounterReason)).join("\n\n") || "(none)"}
60
-
61
- REPOMIX CONTEXT (${input.pack.filePaths.length} files, ${input.pack.totalTokens} tokens):
62
- ${input.pack.text || "(none)"}`;
63
- }
64
-
65
- function formatSearchPattern(check: StupifyCheck): string {
66
- return `Pattern: ${check.id} (${check.name})
67
- Why this matters: ${check.why}
68
- Question: ${check.searchPrompt ?? check.question}
69
- Look for:
70
- ${check.lookFor.map((signal) => `- ${signal}`).join("\n")}
71
- Ignore when:
72
- ${check.ignoreWhen.map((signal) => `- ${signal}`).join("\n")}
73
- Match examples:
74
- ${(check.searchExamples?.match ?? check.examples?.match ?? []).map((example) => `- ${example}`).join("\n")}
75
- Non-match examples:
76
- ${(check.searchExamples?.nonMatch ?? check.examples?.noMatch ?? []).map((example) => `- ${example}`).join("\n")}`;
77
- }
78
-
79
- function formatSearchTarget(context: SemContext, pattern: StupifyCheck, includeCounterReason: boolean): string {
80
- return `TARGET ${context.targetId}
81
- ASSIGNED ${formatSearchPattern(pattern)}
82
- SEM TARGET:
83
- ENTITY ${context.entityId}
84
- NAME ${context.entityName}
85
- KIND ${context.entityKind}
86
- CHANGE ${context.changeKind}
87
- FILE ${context.filePath ?? "(unknown)"}
88
- ${includeCounterReason ? `COUNTER_REASON ${context.reason}` : ""}`.trim();
89
- }
90
-
91
- function patternForContext(context: SemContext, patterns: readonly StupifyCheck[]): StupifyCheck {
92
- return patterns.find((pattern) => pattern.id === context.checkId) ?? {
93
- id: context.checkId,
94
- name: context.checkId,
95
- question: `Does this target match ${context.checkId}?`,
96
- why: "This pattern may indicate judgment-offload.",
97
- lookFor: [],
98
- ignoreWhen: [],
99
- };
100
- }