@stupify/cli 0.0.4 → 0.0.6
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/cache.js +0 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/model.js +3 -3
- package/dist/render.js +1 -1
- package/dist/stupify.js +61 -12
- package/package.json +1 -1
- package/src/cache.ts +0 -2
- package/src/constants.ts +1 -1
- package/src/model.ts +3 -3
- package/src/render.ts +1 -1
- package/src/stupify.ts +57 -12
package/dist/cache.js
CHANGED
|
@@ -10,11 +10,9 @@ export async function cachedJson(namespace, key, compute) {
|
|
|
10
10
|
const filePath = cachePath(namespace, key);
|
|
11
11
|
try {
|
|
12
12
|
const value = JSON.parse(await readFile(filePath, "utf8"));
|
|
13
|
-
console.error(`cache hit ${namespace} ${key.slice(0, 12)}`);
|
|
14
13
|
return value;
|
|
15
14
|
}
|
|
16
15
|
catch {
|
|
17
|
-
console.error(`cache miss ${namespace} ${key.slice(0, 12)}`);
|
|
18
16
|
}
|
|
19
17
|
const value = await compute();
|
|
20
18
|
await writeCache(filePath, value).catch(() => undefined);
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
package/dist/model.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function loadLocalModel(modelPath, modelId, profile = "scout") {
|
|
|
36
36
|
if (runningModel !== modelId)
|
|
37
37
|
await stopManagedServer(runtime);
|
|
38
38
|
if (runningModel === modelId) {
|
|
39
|
-
console.error(`Using
|
|
39
|
+
console.error(`Using local model: ${selectedModel.name}`);
|
|
40
40
|
return {
|
|
41
41
|
id: modelId,
|
|
42
42
|
name: selectedModel.name,
|
|
@@ -107,7 +107,7 @@ async function startLlamaServer(modelPath, modelId, modelName, runtime) {
|
|
|
107
107
|
const logPath = path.join(logDir, "llama-server.log");
|
|
108
108
|
const out = await open(logPath, "a");
|
|
109
109
|
const err = await open(logPath, "a");
|
|
110
|
-
console.error(`Starting local
|
|
110
|
+
console.error(`Starting local model server: ${modelName}`);
|
|
111
111
|
console.error(`llama-server log: ${logPath}`);
|
|
112
112
|
const args = [
|
|
113
113
|
"-m",
|
|
@@ -158,7 +158,7 @@ async function stopManagedServer(runtime) {
|
|
|
158
158
|
throw new Error(`A llama-server is already running with ${runningModel ?? "another model"}.
|
|
159
159
|
Stop it before switching models, or use STUPIFY_LLAMA_SERVER_URL for that server.`);
|
|
160
160
|
}
|
|
161
|
-
console.error(
|
|
161
|
+
console.error("Restarting local model server for selected model.");
|
|
162
162
|
try {
|
|
163
163
|
process.kill(pid, "SIGTERM");
|
|
164
164
|
}
|
package/dist/render.js
CHANGED
|
@@ -12,7 +12,7 @@ ${run.stats.inputTokenCap ?? "unknown"} tokens
|
|
|
12
12
|
Stupify skipped the search rather than review truncated context.
|
|
13
13
|
Nothing was blocked.
|
|
14
14
|
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
18
|
return `🧙 stupify 🪄
|
package/dist/stupify.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { countPromptTokens, runSearch, searchRequest } from "./analysis.js";
|
|
4
5
|
import { searchChecks } from "./checks.js";
|
|
@@ -47,12 +48,9 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
47
48
|
const t = createTracer({
|
|
48
49
|
writeLine: () => undefined,
|
|
49
50
|
onEvent: (event) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (event.detail)
|
|
54
|
-
parts.push(event.detail);
|
|
55
|
-
console.error(parts.join(" "));
|
|
51
|
+
if (command.json)
|
|
52
|
+
return;
|
|
53
|
+
console.error(formatStep(event.name, event.ms, event.count, event.detail));
|
|
56
54
|
},
|
|
57
55
|
});
|
|
58
56
|
const profile = await loadSearchProfile(command.searchProfilePath);
|
|
@@ -60,12 +58,12 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
60
58
|
const patternIds = checks.map((check) => check.id);
|
|
61
59
|
const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
|
|
62
60
|
const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
|
|
61
|
+
printRunPlan(command, patternIds);
|
|
63
62
|
const { value: changeSet } = await t.trace("entity.diff", () => semChangeSetForCommand(command), {
|
|
64
63
|
count: (v) => v.summary.total,
|
|
65
64
|
detail: (v) => `${v.summary.fileCount} files`,
|
|
66
65
|
});
|
|
67
66
|
try {
|
|
68
|
-
printRunPlan(command, changeSet.summary.fileCount, changeSet.summary.total, patternIds);
|
|
69
67
|
const candidates = counterScoutTargets(changeSet, checks, maxCandidates);
|
|
70
68
|
const contexts = entityContextsFromChanges(candidates, changeSet.changes);
|
|
71
69
|
const targetsByPattern = countTargetsByPattern(contexts);
|
|
@@ -135,9 +133,38 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
135
133
|
const pack = profile?.context === "sem" || searchContexts.length === contexts.length
|
|
136
134
|
? initialPack
|
|
137
135
|
: await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
|
|
136
|
+
const request = buildSearchRequest(changeSet, searchContexts, pack, checks, profile, command.includeCounterReasonInPrompt);
|
|
137
|
+
const estimatedInputTokens = estimatePromptTokens(request.prompt);
|
|
138
|
+
if (estimatedInputTokens > maxSearchInputTokens) {
|
|
139
|
+
return {
|
|
140
|
+
schemaVersion: "search.v1",
|
|
141
|
+
mode: "search",
|
|
142
|
+
source: command.source,
|
|
143
|
+
model: { id: command.model },
|
|
144
|
+
patterns: patternIds,
|
|
145
|
+
stats: {
|
|
146
|
+
elapsedMs: Date.now() - startedAt,
|
|
147
|
+
modelCalls: 0,
|
|
148
|
+
inputTokens: estimatedInputTokens,
|
|
149
|
+
inputTokenCap: maxSearchInputTokens,
|
|
150
|
+
skipped: true,
|
|
151
|
+
skipReason: "input_too_large",
|
|
152
|
+
filesChanged: changeSet.summary.fileCount,
|
|
153
|
+
entitiesScanned: changeSet.summary.total,
|
|
154
|
+
candidates: contexts.length,
|
|
155
|
+
searchTargets: searchContexts.length,
|
|
156
|
+
repomixFiles: pack.filePaths.length,
|
|
157
|
+
repomixTokens: pack.totalTokens,
|
|
158
|
+
repomixConfig: pack.config,
|
|
159
|
+
profileId: profile?.id,
|
|
160
|
+
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
161
|
+
targetsPreview: previewTargets(searchContexts),
|
|
162
|
+
},
|
|
163
|
+
matches: [],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
138
166
|
const modelPath = await firstRunModelBootstrap(command.model);
|
|
139
167
|
const model = await loadLocalModel(modelPath, command.model, "scout");
|
|
140
|
-
const request = buildSearchRequest(changeSet, searchContexts, pack, checks, profile, command.includeCounterReasonInPrompt);
|
|
141
168
|
const inputTokens = await countPromptTokens(model, request.prompt);
|
|
142
169
|
if (inputTokens > maxSearchInputTokens) {
|
|
143
170
|
return {
|
|
@@ -206,14 +233,36 @@ function buildSearchRequest(changeSet, contexts, pack, patterns, profile, includ
|
|
|
206
233
|
includeCounterReasonInPrompt: profile?.includeCounterReasonInPrompt ?? includeCounterReasonInPrompt,
|
|
207
234
|
});
|
|
208
235
|
}
|
|
209
|
-
function printRunPlan(command,
|
|
236
|
+
function printRunPlan(command, patternIds) {
|
|
210
237
|
if (command.json)
|
|
211
238
|
return;
|
|
212
239
|
console.error("🧙 stupify 🪄");
|
|
213
|
-
console.error(`
|
|
214
|
-
console.error(`Sem: ${filesChanged} files, ${entitiesScanned} changed entities`);
|
|
240
|
+
console.error(`Search: ${sourceLabel(command)}`);
|
|
215
241
|
console.error(`Patterns: ${patternIds.join(", ")}`);
|
|
216
242
|
}
|
|
243
|
+
function formatStep(name, ms, count, detail) {
|
|
244
|
+
if (name === "entity.diff")
|
|
245
|
+
return `Diff: ${detail ?? "changed files"}, ${count ?? 0} changed entities (${ms}ms)`;
|
|
246
|
+
if (name === "context.pack")
|
|
247
|
+
return `Context: ${count ?? 0} files, ${detail ?? "0 tokens"} (${ms}ms)`;
|
|
248
|
+
if (name === "search.model")
|
|
249
|
+
return `Model: ${count ?? 0} matches (${ms}ms)`;
|
|
250
|
+
return `${name}: ${ms}ms`;
|
|
251
|
+
}
|
|
252
|
+
function sourceLabel(command) {
|
|
253
|
+
if (command.kind === "since")
|
|
254
|
+
return `since ${command.since}`;
|
|
255
|
+
if (command.kind === "commit")
|
|
256
|
+
return `commit ${command.commit}`;
|
|
257
|
+
if (command.kind === "commits")
|
|
258
|
+
return `last ${command.count} commits`;
|
|
259
|
+
if (command.kind === "staged")
|
|
260
|
+
return "staged changes";
|
|
261
|
+
return "stdin diff";
|
|
262
|
+
}
|
|
263
|
+
function estimatePromptTokens(prompt) {
|
|
264
|
+
return Math.ceil(prompt.length / 4);
|
|
265
|
+
}
|
|
217
266
|
function countTargetsByPattern(contexts) {
|
|
218
267
|
const counts = {};
|
|
219
268
|
for (const context of contexts)
|
|
@@ -232,6 +281,6 @@ function pathKind(filePath) {
|
|
|
232
281
|
const ext = filePath.split(".").pop();
|
|
233
282
|
return ext && ext !== filePath ? ext : "unknown";
|
|
234
283
|
}
|
|
235
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
284
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
236
285
|
process.exitCode = await main();
|
|
237
286
|
}
|
package/package.json
CHANGED
package/src/cache.ts
CHANGED
|
@@ -16,10 +16,8 @@ export async function cachedJson<T>(
|
|
|
16
16
|
const filePath = cachePath(namespace, key);
|
|
17
17
|
try {
|
|
18
18
|
const value = JSON.parse(await readFile(filePath, "utf8")) as T;
|
|
19
|
-
console.error(`cache hit ${namespace} ${key.slice(0, 12)}`);
|
|
20
19
|
return value;
|
|
21
20
|
} catch {
|
|
22
|
-
console.error(`cache miss ${namespace} ${key.slice(0, 12)}`);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
const value = await compute();
|
package/src/constants.ts
CHANGED
package/src/model.ts
CHANGED
|
@@ -85,7 +85,7 @@ export async function loadLocalModel(
|
|
|
85
85
|
if (runningModel !== modelId) await stopManagedServer(runtime);
|
|
86
86
|
if (runningModel === modelId) {
|
|
87
87
|
console.error(
|
|
88
|
-
`Using
|
|
88
|
+
`Using local model: ${selectedModel.name}`,
|
|
89
89
|
);
|
|
90
90
|
return {
|
|
91
91
|
id: modelId,
|
|
@@ -166,7 +166,7 @@ async function startLlamaServer(
|
|
|
166
166
|
const out = await open(logPath, "a");
|
|
167
167
|
const err = await open(logPath, "a");
|
|
168
168
|
|
|
169
|
-
console.error(`Starting local
|
|
169
|
+
console.error(`Starting local model server: ${modelName}`);
|
|
170
170
|
console.error(`llama-server log: ${logPath}`);
|
|
171
171
|
|
|
172
172
|
const args = [
|
|
@@ -215,7 +215,7 @@ Stop it before switching models, or use STUPIFY_LLAMA_SERVER_URL for that server
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
console.error(
|
|
218
|
-
|
|
218
|
+
"Restarting local model server for selected model.",
|
|
219
219
|
);
|
|
220
220
|
try {
|
|
221
221
|
process.kill(pid, "SIGTERM");
|
package/src/render.ts
CHANGED
|
@@ -14,7 +14,7 @@ ${run.stats.inputTokenCap ?? "unknown"} tokens
|
|
|
14
14
|
Stupify skipped the search rather than review truncated context.
|
|
15
15
|
Nothing was blocked.
|
|
16
16
|
Try:
|
|
17
|
-
|
|
17
|
+
rerun with ${sourceHint(command)} --max-search-input-tokens ${Math.max((run.stats.inputTokens ?? 12_000) + 1, (run.stats.inputTokenCap ?? 12_000) * 2)}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
if (run.stats.skipped && run.stats.skipReason === "no_candidates") {
|
package/src/stupify.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { countPromptTokens, runSearch, searchRequest } from "./analysis.ts";
|
|
5
6
|
import { searchChecks } from "./checks.ts";
|
|
@@ -57,10 +58,8 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
57
58
|
const t = createTracer({
|
|
58
59
|
writeLine: () => undefined,
|
|
59
60
|
onEvent: (event) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (event.detail) parts.push(event.detail);
|
|
63
|
-
console.error(parts.join(" "));
|
|
61
|
+
if (command.json) return;
|
|
62
|
+
console.error(formatStep(event.name, event.ms, event.count, event.detail));
|
|
64
63
|
},
|
|
65
64
|
});
|
|
66
65
|
|
|
@@ -69,6 +68,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
69
68
|
const patternIds = checks.map((check) => check.id);
|
|
70
69
|
const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
|
|
71
70
|
const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
|
|
71
|
+
printRunPlan(command, patternIds);
|
|
72
72
|
const { value: changeSet } = await t.trace(
|
|
73
73
|
"entity.diff",
|
|
74
74
|
() => semChangeSetForCommand(command),
|
|
@@ -79,7 +79,6 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
79
79
|
);
|
|
80
80
|
|
|
81
81
|
try {
|
|
82
|
-
printRunPlan(command, changeSet.summary.fileCount, changeSet.summary.total, patternIds);
|
|
83
82
|
const candidates = counterScoutTargets(changeSet, checks, maxCandidates);
|
|
84
83
|
const contexts = entityContextsFromChanges(candidates, changeSet.changes);
|
|
85
84
|
const targetsByPattern = countTargetsByPattern(contexts);
|
|
@@ -155,8 +154,6 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
155
154
|
? initialPack
|
|
156
155
|
: await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
|
|
157
156
|
|
|
158
|
-
const modelPath = await firstRunModelBootstrap(command.model);
|
|
159
|
-
const model = await loadLocalModel(modelPath, command.model, "scout");
|
|
160
157
|
const request = buildSearchRequest(
|
|
161
158
|
changeSet,
|
|
162
159
|
searchContexts,
|
|
@@ -165,6 +162,38 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
165
162
|
profile,
|
|
166
163
|
command.includeCounterReasonInPrompt,
|
|
167
164
|
);
|
|
165
|
+
const estimatedInputTokens = estimatePromptTokens(request.prompt);
|
|
166
|
+
if (estimatedInputTokens > maxSearchInputTokens) {
|
|
167
|
+
return {
|
|
168
|
+
schemaVersion: "search.v1",
|
|
169
|
+
mode: "search",
|
|
170
|
+
source: command.source,
|
|
171
|
+
model: { id: command.model },
|
|
172
|
+
patterns: patternIds,
|
|
173
|
+
stats: {
|
|
174
|
+
elapsedMs: Date.now() - startedAt,
|
|
175
|
+
modelCalls: 0,
|
|
176
|
+
inputTokens: estimatedInputTokens,
|
|
177
|
+
inputTokenCap: maxSearchInputTokens,
|
|
178
|
+
skipped: true,
|
|
179
|
+
skipReason: "input_too_large",
|
|
180
|
+
filesChanged: changeSet.summary.fileCount,
|
|
181
|
+
entitiesScanned: changeSet.summary.total,
|
|
182
|
+
candidates: contexts.length,
|
|
183
|
+
searchTargets: searchContexts.length,
|
|
184
|
+
repomixFiles: pack.filePaths.length,
|
|
185
|
+
repomixTokens: pack.totalTokens,
|
|
186
|
+
repomixConfig: pack.config,
|
|
187
|
+
profileId: profile?.id,
|
|
188
|
+
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
189
|
+
targetsPreview: previewTargets(searchContexts),
|
|
190
|
+
},
|
|
191
|
+
matches: [],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const modelPath = await firstRunModelBootstrap(command.model);
|
|
196
|
+
const model = await loadLocalModel(modelPath, command.model, "scout");
|
|
168
197
|
const inputTokens = await countPromptTokens(model, request.prompt);
|
|
169
198
|
if (inputTokens > maxSearchInputTokens) {
|
|
170
199
|
return {
|
|
@@ -249,17 +278,33 @@ function buildSearchRequest(
|
|
|
249
278
|
|
|
250
279
|
function printRunPlan(
|
|
251
280
|
command: SearchCommand,
|
|
252
|
-
filesChanged: number,
|
|
253
|
-
entitiesScanned: number,
|
|
254
281
|
patternIds: readonly string[],
|
|
255
282
|
): void {
|
|
256
283
|
if (command.json) return;
|
|
257
284
|
console.error("🧙 stupify 🪄");
|
|
258
|
-
console.error(`
|
|
259
|
-
console.error(`Sem: ${filesChanged} files, ${entitiesScanned} changed entities`);
|
|
285
|
+
console.error(`Search: ${sourceLabel(command)}`);
|
|
260
286
|
console.error(`Patterns: ${patternIds.join(", ")}`);
|
|
261
287
|
}
|
|
262
288
|
|
|
289
|
+
function formatStep(name: string, ms: number, count?: number, detail?: string): string {
|
|
290
|
+
if (name === "entity.diff") return `Diff: ${detail ?? "changed files"}, ${count ?? 0} changed entities (${ms}ms)`;
|
|
291
|
+
if (name === "context.pack") return `Context: ${count ?? 0} files, ${detail ?? "0 tokens"} (${ms}ms)`;
|
|
292
|
+
if (name === "search.model") return `Model: ${count ?? 0} matches (${ms}ms)`;
|
|
293
|
+
return `${name}: ${ms}ms`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function sourceLabel(command: SearchCommand): string {
|
|
297
|
+
if (command.kind === "since") return `since ${command.since}`;
|
|
298
|
+
if (command.kind === "commit") return `commit ${command.commit}`;
|
|
299
|
+
if (command.kind === "commits") return `last ${command.count} commits`;
|
|
300
|
+
if (command.kind === "staged") return "staged changes";
|
|
301
|
+
return "stdin diff";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function estimatePromptTokens(prompt: string): number {
|
|
305
|
+
return Math.ceil(prompt.length / 4);
|
|
306
|
+
}
|
|
307
|
+
|
|
263
308
|
function countTargetsByPattern(contexts: readonly SemContext[]): Record<string, number> {
|
|
264
309
|
const counts: Record<string, number> = {};
|
|
265
310
|
for (const context of contexts) counts[context.checkId] = (counts[context.checkId] ?? 0) + 1;
|
|
@@ -280,6 +325,6 @@ function pathKind(filePath: string): string {
|
|
|
280
325
|
return ext && ext !== filePath ? ext : "unknown";
|
|
281
326
|
}
|
|
282
327
|
|
|
283
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
328
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
284
329
|
process.exitCode = await main();
|
|
285
330
|
}
|