@stupify/cli 0.0.9 → 0.0.11
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/analysis.js +30 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/model.d.ts +3 -2
- package/dist/model.js +42 -43
- package/dist/render.js +33 -23
- package/dist/sem-provider.js +6 -5
- package/dist/stupify.d.ts +35 -1
- package/dist/stupify.js +24 -19
- package/dist/types.d.ts +1 -0
- package/dist/ui.d.ts +34 -0
- package/dist/ui.js +143 -0
- package/package.json +3 -1
- package/src/analysis.ts +29 -2
- package/src/constants.ts +1 -1
- package/src/model.ts +49 -55
- package/src/render.ts +35 -23
- package/src/sem-provider.ts +6 -5
- package/src/stupify.ts +27 -18
- package/src/types.ts +1 -0
- package/src/ui.ts +187 -0
package/dist/analysis.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cachedJson, fingerprint } from "./cache.js";
|
|
2
2
|
import { searchPrompt } from "./prompts.js";
|
|
3
|
+
import { diagnostic, diagnosticError } from "./ui.js";
|
|
3
4
|
export async function runSearch(model, request) {
|
|
4
5
|
const raw = await runJsonPrompt(model, request.prompt, request.schema, 0);
|
|
5
6
|
return uncheckedSearchMatches(raw, request.contexts);
|
|
@@ -72,6 +73,7 @@ function uncheckedSearchMatches(value, contexts) {
|
|
|
72
73
|
patternId: context.checkId,
|
|
73
74
|
reason: match.reason ?? "",
|
|
74
75
|
proof: sourcePointer(context),
|
|
76
|
+
snapshot: sourceSnapshot(context),
|
|
75
77
|
}];
|
|
76
78
|
});
|
|
77
79
|
}
|
|
@@ -79,6 +81,32 @@ function sourcePointer(context) {
|
|
|
79
81
|
const file = context.filePath ?? "(unknown)";
|
|
80
82
|
return `${file}::${context.entityKind || "entity"}::${context.entityName || context.entityId}`;
|
|
81
83
|
}
|
|
84
|
+
function sourceSnapshot(context) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(context.text);
|
|
87
|
+
const snapshot = stringSnapshot(parsed.after) ?? stringSnapshot(parsed.before);
|
|
88
|
+
return snapshot ? limitSnapshot(snapshot) : undefined;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function stringSnapshot(value) {
|
|
95
|
+
if (typeof value !== "string")
|
|
96
|
+
return undefined;
|
|
97
|
+
const trimmed = value.trim();
|
|
98
|
+
if (!trimmed || trimmed === "(none)")
|
|
99
|
+
return undefined;
|
|
100
|
+
return trimmed;
|
|
101
|
+
}
|
|
102
|
+
function limitSnapshot(value) {
|
|
103
|
+
const lines = value.split(/\r?\n/);
|
|
104
|
+
const limit = 24;
|
|
105
|
+
if (lines.length <= limit)
|
|
106
|
+
return value;
|
|
107
|
+
return `${lines.slice(0, limit).join("\n")}
|
|
108
|
+
[stupify: snapshot shortened after ${limit} lines]`;
|
|
109
|
+
}
|
|
82
110
|
async function runJsonPrompt(model, prompt, schema, temperature) {
|
|
83
111
|
return cachedJson("model-json", fingerprint({
|
|
84
112
|
version: 1,
|
|
@@ -100,8 +128,8 @@ Your previous response was not valid JSON. Return the requested JSON object only
|
|
|
100
128
|
const retryParsed = parseJson(retry);
|
|
101
129
|
if (retryParsed.ok)
|
|
102
130
|
return retryParsed.value;
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
diagnosticError("Raw model output:");
|
|
132
|
+
diagnostic(retry);
|
|
105
133
|
throw new Error("Model returned invalid JSON.");
|
|
106
134
|
}
|
|
107
135
|
async function complete(model, prompt, schema, temperature) {
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
package/dist/model.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModelId } from "./types.ts";
|
|
2
|
+
import type { CliUi } from "./ui.ts";
|
|
2
3
|
export type ModelProfile = "scout";
|
|
3
4
|
export type LocalModel = Readonly<{
|
|
4
5
|
id: ModelId;
|
|
@@ -6,5 +7,5 @@ export type LocalModel = Readonly<{
|
|
|
6
7
|
baseUrl: string;
|
|
7
8
|
profile: ModelProfile;
|
|
8
9
|
}>;
|
|
9
|
-
export declare function firstRunModelBootstrap(modelId: ModelId): Promise<string>;
|
|
10
|
-
export declare function loadLocalModel(modelPath: string, modelId: ModelId, profile
|
|
10
|
+
export declare function firstRunModelBootstrap(modelId: ModelId, ui: CliUi): Promise<string>;
|
|
11
|
+
export declare function loadLocalModel(modelPath: string, modelId: ModelId, profile: ModelProfile, ui: CliUi): Promise<LocalModel>;
|
package/dist/model.js
CHANGED
|
@@ -1,42 +1,39 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { createReadStream, createWriteStream } from "node:fs";
|
|
3
2
|
import { mkdir, open, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
3
|
import { homedir, platform } from "node:os";
|
|
5
|
-
import { stdin as input, stderr as statusOutput, stdout as output, } from "node:process";
|
|
6
|
-
import { createInterface } from "node:readline/promises";
|
|
7
4
|
import path from "node:path";
|
|
8
5
|
import { promisify } from "node:util";
|
|
9
6
|
import { MODEL_REGISTRY } from "./constants.js";
|
|
10
7
|
const execFileAsync = promisify(execFile);
|
|
11
8
|
const LLAMA_SERVER_HOST = "127.0.0.1";
|
|
12
|
-
export async function firstRunModelBootstrap(modelId) {
|
|
9
|
+
export async function firstRunModelBootstrap(modelId, ui) {
|
|
13
10
|
const selectedModel = MODEL_REGISTRY[modelId];
|
|
14
11
|
const modelDir = path.join(cacheDir(), "models");
|
|
15
12
|
const modelPath = path.join(modelDir, selectedModel.file);
|
|
16
13
|
if (await exists(modelPath))
|
|
17
14
|
return modelPath;
|
|
18
|
-
|
|
15
|
+
ui.note(`No local Stupify model found.
|
|
19
16
|
Stupify runs locally.
|
|
20
17
|
Download this model now?
|
|
21
18
|
Model: ${selectedModel.name}
|
|
22
|
-
Size: ${selectedModel.size}
|
|
23
|
-
if (!(await confirm("Continue?
|
|
19
|
+
Size: ${selectedModel.size}`, "Setup", { force: true });
|
|
20
|
+
if (!(await ui.confirm("Continue?")))
|
|
24
21
|
throw new Error("Setup cancelled.");
|
|
25
22
|
await mkdir(modelDir, { recursive: true });
|
|
26
|
-
await downloadModel(modelPath, selectedModel.url);
|
|
23
|
+
await downloadModel(modelPath, selectedModel.url, ui);
|
|
27
24
|
if (!(await exists(modelPath)))
|
|
28
25
|
throw new Error("Model download failed: file was not created.");
|
|
29
26
|
return modelPath;
|
|
30
27
|
}
|
|
31
|
-
export async function loadLocalModel(modelPath, modelId, profile
|
|
28
|
+
export async function loadLocalModel(modelPath, modelId, profile, ui) {
|
|
32
29
|
const selectedModel = MODEL_REGISTRY[modelId];
|
|
33
30
|
const runtime = modelRuntime(profile);
|
|
34
31
|
const runningModel = await runningServerModel(runtime.baseUrl);
|
|
35
32
|
if (runningModel) {
|
|
36
33
|
if (runningModel !== modelId)
|
|
37
|
-
await stopManagedServer(runtime);
|
|
34
|
+
await stopManagedServer(runtime, ui);
|
|
38
35
|
if (runningModel === modelId) {
|
|
39
|
-
|
|
36
|
+
ui.info(`Using local model: ${selectedModel.name}`);
|
|
40
37
|
return {
|
|
41
38
|
id: modelId,
|
|
42
39
|
name: selectedModel.name,
|
|
@@ -46,8 +43,16 @@ export async function loadLocalModel(modelPath, modelId, profile = "scout") {
|
|
|
46
43
|
}
|
|
47
44
|
}
|
|
48
45
|
await ensureLlamaServerBinary();
|
|
49
|
-
await startLlamaServer(modelPath, modelId, selectedModel.name, runtime);
|
|
50
|
-
|
|
46
|
+
await startLlamaServer(modelPath, modelId, selectedModel.name, runtime, ui);
|
|
47
|
+
const ready = ui.spinner(`Waiting for local ${profile} model`);
|
|
48
|
+
try {
|
|
49
|
+
await waitForServer(runtime.baseUrl, modelId);
|
|
50
|
+
ready.stop(`Local ${profile} model ready`);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
ready.error(`Local ${profile} model failed to start`);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
51
56
|
return {
|
|
52
57
|
id: modelId,
|
|
53
58
|
name: selectedModel.name,
|
|
@@ -101,14 +106,14 @@ Install llama.cpp first:
|
|
|
101
106
|
brew install llama.cpp`);
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
|
-
async function startLlamaServer(modelPath, modelId, modelName, runtime) {
|
|
109
|
+
async function startLlamaServer(modelPath, modelId, modelName, runtime, ui) {
|
|
105
110
|
const logDir = path.join(cacheDir(), "logs");
|
|
106
111
|
await mkdir(logDir, { recursive: true });
|
|
107
112
|
const logPath = path.join(logDir, "llama-server.log");
|
|
108
113
|
const out = await open(logPath, "a");
|
|
109
114
|
const err = await open(logPath, "a");
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
ui.step(`Starting local model server: ${modelName}`);
|
|
116
|
+
ui.info(`llama-server log: ${logPath}`);
|
|
112
117
|
const args = [
|
|
113
118
|
"-m",
|
|
114
119
|
modelPath,
|
|
@@ -151,14 +156,14 @@ async function startLlamaServer(modelPath, modelId, modelName, runtime) {
|
|
|
151
156
|
await out.close();
|
|
152
157
|
await err.close();
|
|
153
158
|
}
|
|
154
|
-
async function stopManagedServer(runtime) {
|
|
159
|
+
async function stopManagedServer(runtime, ui) {
|
|
155
160
|
const pid = await managedServerPid(runtime);
|
|
156
161
|
if (!pid) {
|
|
157
162
|
const runningModel = await runningServerModel(runtime.baseUrl);
|
|
158
163
|
throw new Error(`A llama-server is already running with ${runningModel ?? "another model"}.
|
|
159
164
|
Stop it before switching models, or use STUPIFY_LLAMA_SERVER_URL for that server.`);
|
|
160
165
|
}
|
|
161
|
-
|
|
166
|
+
ui.step("Restarting local model server for selected model.");
|
|
162
167
|
try {
|
|
163
168
|
process.kill(pid, "SIGTERM");
|
|
164
169
|
}
|
|
@@ -214,10 +219,11 @@ async function waitForServer(baseUrl, modelId) {
|
|
|
214
219
|
function sleep(ms) {
|
|
215
220
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
216
221
|
}
|
|
217
|
-
async function downloadModel(modelPath, modelUrl) {
|
|
222
|
+
async function downloadModel(modelPath, modelUrl, ui) {
|
|
218
223
|
const tempPath = `${modelPath}.download`;
|
|
219
224
|
await rm(tempPath, { force: true });
|
|
220
|
-
|
|
225
|
+
const downloadSpinner = ui.spinner("Downloading model");
|
|
226
|
+
let downloadProgress = null;
|
|
221
227
|
try {
|
|
222
228
|
const response = await fetch(modelUrl);
|
|
223
229
|
if (!response.ok || !response.body)
|
|
@@ -225,8 +231,13 @@ async function downloadModel(modelPath, modelUrl) {
|
|
|
225
231
|
const total = Number(response.headers.get("content-length") ?? 0);
|
|
226
232
|
let received = 0;
|
|
227
233
|
let lastPrint = 0;
|
|
234
|
+
let lastProgressBytes = 0;
|
|
228
235
|
const reader = response.body.getReader();
|
|
229
236
|
const file = await open(tempPath, "wx");
|
|
237
|
+
if (total > 0) {
|
|
238
|
+
downloadSpinner.clear();
|
|
239
|
+
downloadProgress = ui.progress("Downloading model", total);
|
|
240
|
+
}
|
|
230
241
|
try {
|
|
231
242
|
while (true) {
|
|
232
243
|
const { done, value } = await reader.read();
|
|
@@ -237,42 +248,30 @@ async function downloadModel(modelPath, modelUrl) {
|
|
|
237
248
|
const now = Date.now();
|
|
238
249
|
if (total > 0 && now - lastPrint > 500) {
|
|
239
250
|
lastPrint = now;
|
|
240
|
-
|
|
251
|
+
downloadProgress?.advance(received - lastProgressBytes, `${formatBytes(received)} / ${formatBytes(total)}`);
|
|
252
|
+
lastProgressBytes = received;
|
|
241
253
|
}
|
|
242
254
|
}
|
|
243
255
|
}
|
|
244
256
|
finally {
|
|
245
257
|
await file.close();
|
|
246
258
|
}
|
|
247
|
-
if (
|
|
248
|
-
|
|
259
|
+
if (downloadProgress && received > lastProgressBytes) {
|
|
260
|
+
downloadProgress.advance(received - lastProgressBytes, `${formatBytes(received)} / ${formatBytes(total)}`);
|
|
261
|
+
}
|
|
262
|
+
const activeProgress = downloadProgress ?? downloadSpinner;
|
|
263
|
+
activeProgress.stop(total > 0
|
|
264
|
+
? `Downloaded ${formatBytes(received)} / ${formatBytes(total)}`
|
|
265
|
+
: "Downloaded model");
|
|
249
266
|
await rename(tempPath, modelPath);
|
|
250
267
|
}
|
|
251
268
|
catch (error) {
|
|
269
|
+
const activeProgress = downloadProgress ?? downloadSpinner;
|
|
270
|
+
activeProgress.error("Model download failed");
|
|
252
271
|
await rm(tempPath, { force: true });
|
|
253
272
|
throw error;
|
|
254
273
|
}
|
|
255
274
|
}
|
|
256
|
-
async function confirm(question) {
|
|
257
|
-
const rl = createInterface(terminalIo());
|
|
258
|
-
try {
|
|
259
|
-
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
260
|
-
return answer === "y" || answer === "yes";
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
rl.close();
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
function terminalIo() {
|
|
267
|
-
if (input.isTTY)
|
|
268
|
-
return { input, output };
|
|
269
|
-
if (platform() !== "win32")
|
|
270
|
-
return {
|
|
271
|
-
input: createReadStream("/dev/tty"),
|
|
272
|
-
output: createWriteStream("/dev/tty"),
|
|
273
|
-
};
|
|
274
|
-
throw new Error("No local Stupify model found. Run `stupify` once in an interactive terminal to set up the model.");
|
|
275
|
-
}
|
|
276
275
|
function cacheDir() {
|
|
277
276
|
if (process.env.STUPIFY_CACHE_DIR)
|
|
278
277
|
return process.env.STUPIFY_CACHE_DIR;
|
package/dist/render.js
CHANGED
|
@@ -1,40 +1,42 @@
|
|
|
1
1
|
import { VERSION } from "./constants.js";
|
|
2
|
+
import { format } from "./ui.js";
|
|
2
3
|
export function renderSearchRun(run, command) {
|
|
3
4
|
if (command.json)
|
|
4
5
|
return JSON.stringify(run, null, 2);
|
|
5
6
|
if (run.stats.skipped && run.stats.skipReason === "input_too_large") {
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
Size:
|
|
7
|
+
return `${format.heading("Search input is too large for precise local search.")}
|
|
8
|
+
${format.heading("Size:")}
|
|
9
9
|
~${run.stats.inputTokens ?? "unknown"} tokens
|
|
10
|
-
Limit:
|
|
10
|
+
${format.heading("Limit:")}
|
|
11
11
|
${run.stats.inputTokenCap ?? "unknown"} tokens
|
|
12
12
|
Stupify skipped the search rather than review truncated context.
|
|
13
13
|
Nothing was blocked.
|
|
14
|
-
Try:
|
|
14
|
+
${format.heading("Try:")}
|
|
15
15
|
rerun with ${sourceHint(command)} --max-search-input-tokens ${Math.max((run.stats.inputTokens ?? 12_000) + 1, (run.stats.inputTokenCap ?? 12_000) * 2)}`;
|
|
16
16
|
}
|
|
17
17
|
if (run.stats.skipped && run.stats.skipReason === "no_candidates") {
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
No search targets found.`;
|
|
18
|
+
return `${format.heading("Search complete.")}
|
|
19
|
+
${format.label("Patterns:")} ${run.patterns.join(", ")}
|
|
20
|
+
${format.success("No search targets found.")}`;
|
|
22
21
|
}
|
|
23
22
|
if (run.matches.length === 0) {
|
|
24
|
-
return
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
No judgment-offload signals found.`;
|
|
23
|
+
return `${format.heading("Search complete.")}
|
|
24
|
+
${format.label("Patterns:")} ${run.patterns.join(", ")}
|
|
25
|
+
${format.success("No judgment-offload signals found.")}`;
|
|
28
26
|
}
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
${run
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
return `${slopHeading()}
|
|
28
|
+
${run.matches.map((match, index) => `${index + 1}. ${format.label(match.patternId)}
|
|
29
|
+
${committerLabel(run)} (${sourceLabel(command)})
|
|
30
|
+
|
|
31
|
+
${match.reason}
|
|
32
|
+
|
|
33
|
+
\`\`\`
|
|
34
|
+
${match.snapshot ?? match.proof}
|
|
35
|
+
\`\`\`
|
|
36
|
+
${format.muted(match.proof)}
|
|
37
|
+
|
|
38
|
+
${match.checkWhy ?? "This pattern may indicate judgment-offload."}`).join("\n\n")}
|
|
39
|
+
${format.muted("Search mode is warn-only.")}`;
|
|
38
40
|
}
|
|
39
41
|
export function helpText() {
|
|
40
42
|
return `Stupify ${VERSION}
|
|
@@ -106,10 +108,18 @@ function sourceLabel(command) {
|
|
|
106
108
|
return "stdin diff";
|
|
107
109
|
}
|
|
108
110
|
function committerLabel(run) {
|
|
109
|
-
const committers = (run.stats.committers ?? []).filter(Boolean);
|
|
111
|
+
const committers = (run.stats.committers ?? []).filter(Boolean).map(committerDisplayName);
|
|
110
112
|
if (committers.length === 0)
|
|
111
113
|
return "unknown committer";
|
|
112
114
|
if (committers.length <= 3)
|
|
113
115
|
return committers.join(", ");
|
|
114
116
|
return `${committers.slice(0, 3).join(", ")} +${committers.length - 3} more`;
|
|
115
117
|
}
|
|
118
|
+
function committerDisplayName(value) {
|
|
119
|
+
return value.replace(/\s*<[^>]+>\s*$/, "").trim() || value;
|
|
120
|
+
}
|
|
121
|
+
function slopHeading() {
|
|
122
|
+
const heading = "AI SLOP DETECTED";
|
|
123
|
+
return `${format.warn(format.heading(heading))}
|
|
124
|
+
${format.warn("=".repeat(heading.length))}`;
|
|
125
|
+
}
|
package/dist/sem-provider.js
CHANGED
|
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
|
|
|
7
7
|
import { cachedJson, fingerprint } from "./cache.js";
|
|
8
8
|
import { readDiffFromStdin } from "./diff.js";
|
|
9
9
|
import { gitUserLabel, sourceRangeForCommit, sourceRangeForRecentCommits, sourceRangeSince, stagedDiff, } from "./git.js";
|
|
10
|
+
import { diagnostic } from "./ui.js";
|
|
10
11
|
import { sourceId } from "./types.js";
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
12
13
|
export async function semChangeSetForCommand(command) {
|
|
@@ -108,7 +109,7 @@ async function withContextWorkspace(changeSet, debugSem) {
|
|
|
108
109
|
}
|
|
109
110
|
async function runSem(args, debugSem, cwd = process.cwd()) {
|
|
110
111
|
if (debugSem)
|
|
111
|
-
|
|
112
|
+
diagnostic(`sem ${args.join(" ")}`);
|
|
112
113
|
const { command, commandArgs } = resolveSemCommand(args);
|
|
113
114
|
try {
|
|
114
115
|
const { stdout, stderr } = await execFileAsync(command, commandArgs, {
|
|
@@ -116,7 +117,7 @@ async function runSem(args, debugSem, cwd = process.cwd()) {
|
|
|
116
117
|
maxBuffer: 128 * 1024 * 1024,
|
|
117
118
|
});
|
|
118
119
|
if (debugSem && stderr.trim())
|
|
119
|
-
|
|
120
|
+
diagnostic(stderr.trim());
|
|
120
121
|
return JSON.parse(stdout);
|
|
121
122
|
}
|
|
122
123
|
catch (error) {
|
|
@@ -126,7 +127,7 @@ async function runSem(args, debugSem, cwd = process.cwd()) {
|
|
|
126
127
|
}
|
|
127
128
|
async function runSemWithInput(args, stdin, debugSem) {
|
|
128
129
|
if (debugSem)
|
|
129
|
-
|
|
130
|
+
diagnostic(`sem ${args.join(" ")}`);
|
|
130
131
|
const { command, commandArgs } = resolveSemCommand(args);
|
|
131
132
|
return new Promise((resolve, reject) => {
|
|
132
133
|
const child = spawn(command, commandArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -138,7 +139,7 @@ async function runSemWithInput(args, stdin, debugSem) {
|
|
|
138
139
|
child.on("close", (code) => {
|
|
139
140
|
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
140
141
|
if (debugSem && stderrText.trim())
|
|
141
|
-
|
|
142
|
+
diagnostic(stderrText.trim());
|
|
142
143
|
if (code !== 0) {
|
|
143
144
|
reject(new Error(`sem failed with exit code ${code}${stderrText ? `: ${stderrText.trim()}` : ""}`));
|
|
144
145
|
return;
|
|
@@ -155,7 +156,7 @@ async function runSemWithInput(args, stdin, debugSem) {
|
|
|
155
156
|
}
|
|
156
157
|
async function git(args, debugSem) {
|
|
157
158
|
if (debugSem)
|
|
158
|
-
|
|
159
|
+
diagnostic(`git ${args.join(" ")}`);
|
|
159
160
|
await execFileAsync("git", [...args], { maxBuffer: 128 * 1024 * 1024 });
|
|
160
161
|
}
|
|
161
162
|
async function cleanupWorktree(tempDir, worktreeAdded, debugSem) {
|
package/dist/stupify.d.ts
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import type { SearchCommand, SearchRunJson } from "./types.ts";
|
|
3
3
|
export declare function main(argv?: string[]): Promise<number>;
|
|
4
|
-
export declare function runSearchCommand(command: SearchCommand, startedAt: number
|
|
4
|
+
export declare function runSearchCommand(command: SearchCommand, startedAt: number, ui?: {
|
|
5
|
+
intro(title: string, logOptions?: Readonly<{
|
|
6
|
+
force?: boolean;
|
|
7
|
+
}>): void;
|
|
8
|
+
outro(message: string, logOptions?: Readonly<{
|
|
9
|
+
force?: boolean;
|
|
10
|
+
}>): void;
|
|
11
|
+
note(message: string, title?: string, logOptions?: Readonly<{
|
|
12
|
+
force?: boolean;
|
|
13
|
+
}>): void;
|
|
14
|
+
info(message: string, logOptions?: Readonly<{
|
|
15
|
+
force?: boolean;
|
|
16
|
+
}>): void;
|
|
17
|
+
step(message: string, logOptions?: Readonly<{
|
|
18
|
+
force?: boolean;
|
|
19
|
+
}>): void;
|
|
20
|
+
success(message: string, logOptions?: Readonly<{
|
|
21
|
+
force?: boolean;
|
|
22
|
+
}>): void;
|
|
23
|
+
warn(message: string, logOptions?: Readonly<{
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}>): void;
|
|
26
|
+
error(message: string, logOptions?: Readonly<{
|
|
27
|
+
force?: boolean;
|
|
28
|
+
}>): void;
|
|
29
|
+
debug(message: string): void;
|
|
30
|
+
confirm(message: string): Promise<boolean>;
|
|
31
|
+
spinner(message: string, logOptions?: Readonly<{
|
|
32
|
+
force?: boolean;
|
|
33
|
+
}>): import("@clack/prompts").SpinnerResult;
|
|
34
|
+
progress(message: string, max: number, logOptions?: Readonly<{
|
|
35
|
+
force?: boolean;
|
|
36
|
+
}>): import("@clack/prompts").ProgressResult;
|
|
37
|
+
writeStdout(text: string): void;
|
|
38
|
+
}): Promise<SearchRunJson>;
|
package/dist/stupify.js
CHANGED
|
@@ -13,44 +13,47 @@ import { helpText, renderSearchRun } from "./render.js";
|
|
|
13
13
|
import { effectiveMaxCandidates, effectiveMaxSearchInputTokens, effectiveRepomixConfig, effectiveSearchChecks, loadSearchProfile, } from "./search-profile.js";
|
|
14
14
|
import { semChangeSetForCommand } from "./sem-provider.js";
|
|
15
15
|
import { createTracer } from "./trace.js";
|
|
16
|
+
import { createCliUi } from "./ui.js";
|
|
16
17
|
export async function main(argv = process.argv.slice(2)) {
|
|
17
18
|
const startedAt = Date.now();
|
|
19
|
+
let ui = createCliUi();
|
|
18
20
|
try {
|
|
19
21
|
const command = parseCommand(argv);
|
|
20
22
|
if (command.kind === "help") {
|
|
21
|
-
|
|
23
|
+
ui.writeStdout(helpText());
|
|
22
24
|
return 0;
|
|
23
25
|
}
|
|
24
26
|
if (command.kind === "hook") {
|
|
25
|
-
|
|
27
|
+
ui.writeStdout(await runHookCommand(command.action));
|
|
26
28
|
return 0;
|
|
27
29
|
}
|
|
28
30
|
if (command.kind === "doctor") {
|
|
29
31
|
const result = await runDoctor();
|
|
30
|
-
|
|
32
|
+
ui.writeStdout(result.text);
|
|
31
33
|
return result.exitCode;
|
|
32
34
|
}
|
|
33
35
|
if (command.kind === "bench-search") {
|
|
34
36
|
const { runSearchBench } = await import("./search-bench.js");
|
|
35
|
-
|
|
37
|
+
ui.writeStdout(await runSearchBench(command.configPath));
|
|
36
38
|
return 0;
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
ui = createCliUi({ quiet: command.json });
|
|
41
|
+
const run = await runSearchCommand(command, startedAt, ui);
|
|
42
|
+
ui.writeStdout(renderSearchRun(run, command));
|
|
40
43
|
return 0;
|
|
41
44
|
}
|
|
42
45
|
catch (error) {
|
|
43
|
-
|
|
46
|
+
ui.error(error instanceof Error ? error.message : String(error), { force: true });
|
|
44
47
|
return 1;
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
|
-
export async function runSearchCommand(command, startedAt) {
|
|
50
|
+
export async function runSearchCommand(command, startedAt, ui = createCliUi({ quiet: command.json })) {
|
|
48
51
|
const t = createTracer({
|
|
49
52
|
writeLine: () => undefined,
|
|
50
53
|
onEvent: (event) => {
|
|
51
54
|
if (command.json)
|
|
52
55
|
return;
|
|
53
|
-
|
|
56
|
+
ui.step(formatStep(event.name, event.ms, event.count, event.detail));
|
|
54
57
|
},
|
|
55
58
|
});
|
|
56
59
|
const profile = await loadSearchProfile(command.searchProfilePath);
|
|
@@ -58,7 +61,7 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
58
61
|
const patternIds = checks.map((check) => check.id);
|
|
59
62
|
const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
|
|
60
63
|
const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
|
|
61
|
-
printRunPlan(command, patternIds);
|
|
64
|
+
printRunPlan(command, patternIds, ui);
|
|
62
65
|
const { value: changeSet } = await t.trace("entity.diff", () => semChangeSetForCommand(command), {
|
|
63
66
|
count: (v) => v.summary.total,
|
|
64
67
|
detail: (v) => `${v.summary.fileCount} files`,
|
|
@@ -178,13 +181,13 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
178
181
|
};
|
|
179
182
|
}
|
|
180
183
|
if (batches.wasSplit && !command.json) {
|
|
181
|
-
|
|
184
|
+
ui.warn(`Search input is large; queued ${batches.batches.length} smaller search batches.`);
|
|
182
185
|
if (batches.skippedTargets > 0) {
|
|
183
|
-
|
|
186
|
+
ui.warn(`Skipped ${batches.skippedTargets} oversized targets that could not fit alone.`);
|
|
184
187
|
}
|
|
185
188
|
}
|
|
186
|
-
const modelPath = await firstRunModelBootstrap(command.model);
|
|
187
|
-
const model = await loadLocalModel(modelPath, command.model, "scout");
|
|
189
|
+
const modelPath = await firstRunModelBootstrap(command.model, ui);
|
|
190
|
+
const model = await loadLocalModel(modelPath, command.model, "scout", ui);
|
|
188
191
|
const matches = [];
|
|
189
192
|
let modelCalls = 0;
|
|
190
193
|
let inputTokens = 0;
|
|
@@ -195,7 +198,7 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
195
198
|
if (batchInputTokens > maxSearchInputTokens) {
|
|
196
199
|
exactSkippedTargets += batch.contexts.length;
|
|
197
200
|
if (!command.json) {
|
|
198
|
-
|
|
201
|
+
ui.warn(`Skipped ${batch.contexts.length} targets after exact token count exceeded the limit.`);
|
|
199
202
|
}
|
|
200
203
|
continue;
|
|
201
204
|
}
|
|
@@ -324,12 +327,14 @@ function buildSearchRequest(changeSet, contexts, pack, patterns, profile, includ
|
|
|
324
327
|
includeCounterReasonInPrompt: profile?.includeCounterReasonInPrompt ?? includeCounterReasonInPrompt,
|
|
325
328
|
});
|
|
326
329
|
}
|
|
327
|
-
function printRunPlan(command, patternIds) {
|
|
330
|
+
function printRunPlan(command, patternIds, ui) {
|
|
328
331
|
if (command.json)
|
|
329
332
|
return;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
+
ui.intro("stupify");
|
|
334
|
+
ui.note([
|
|
335
|
+
`Search: ${sourceLabel(command)}`,
|
|
336
|
+
`Patterns: ${patternIds.join(", ")}`,
|
|
337
|
+
].join("\n"), "Run");
|
|
333
338
|
}
|
|
334
339
|
function formatStep(name, ms, count, detail) {
|
|
335
340
|
if (name === "entity.diff")
|
package/dist/types.d.ts
CHANGED
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type SpinnerResult } from "@clack/prompts";
|
|
2
|
+
export type CliUi = ReturnType<typeof createCliUi>;
|
|
3
|
+
export type CliUiOptions = Readonly<{
|
|
4
|
+
quiet?: boolean;
|
|
5
|
+
}>;
|
|
6
|
+
type LogOptions = Readonly<{
|
|
7
|
+
force?: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function createCliUi(options?: CliUiOptions): {
|
|
10
|
+
intro(title: string, logOptions?: LogOptions): void;
|
|
11
|
+
outro(message: string, logOptions?: LogOptions): void;
|
|
12
|
+
note(message: string, title?: string, logOptions?: LogOptions): void;
|
|
13
|
+
info(message: string, logOptions?: LogOptions): void;
|
|
14
|
+
step(message: string, logOptions?: LogOptions): void;
|
|
15
|
+
success(message: string, logOptions?: LogOptions): void;
|
|
16
|
+
warn(message: string, logOptions?: LogOptions): void;
|
|
17
|
+
error(message: string, logOptions?: LogOptions): void;
|
|
18
|
+
debug(message: string): void;
|
|
19
|
+
confirm(message: string): Promise<boolean>;
|
|
20
|
+
spinner(message: string, logOptions?: LogOptions): SpinnerResult;
|
|
21
|
+
progress(message: string, max: number, logOptions?: LogOptions): import("@clack/prompts").ProgressResult;
|
|
22
|
+
writeStdout(text: string): void;
|
|
23
|
+
};
|
|
24
|
+
export declare const format: {
|
|
25
|
+
heading: (value: string) => string;
|
|
26
|
+
label: (value: string) => string;
|
|
27
|
+
muted: (value: string) => string;
|
|
28
|
+
success: (value: string) => string;
|
|
29
|
+
warn: (value: string) => string;
|
|
30
|
+
error: (value: string) => string;
|
|
31
|
+
};
|
|
32
|
+
export declare function diagnostic(message: string): void;
|
|
33
|
+
export declare function diagnosticError(message: string): void;
|
|
34
|
+
export {};
|