@stupify/cli 0.0.5 → 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 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);
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.0.5";
1
+ export declare const VERSION = "0.0.6";
2
2
  import type { ModelConfig, ModelId } from "./types.ts";
3
3
  export declare const DEFAULT_MODEL_ID: ModelId;
4
4
  export declare const MODEL_REGISTRY: Record<ModelId, ModelConfig>;
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = "0.0.5";
1
+ export const VERSION = "0.0.6";
2
2
  export const DEFAULT_MODEL_ID = "gemma-4-e2b";
3
3
  export const MODEL_REGISTRY = {
4
4
  "gemma-4-e2b": {
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 already-loaded local ${profile} model: ${selectedModel.name}`);
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 ${runtime.profile} model server: ${modelName}`);
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(`Restarting local ${runtime.profile} model server for selected model.`);
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
- stupify ${sourceHint(command)} --max-search-input-tokens ${Math.max((run.stats.inputTokens ?? 12_000) + 1, (run.stats.inputTokenCap ?? 12_000) * 2)}`;
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
@@ -48,12 +48,9 @@ export async function runSearchCommand(command, startedAt) {
48
48
  const t = createTracer({
49
49
  writeLine: () => undefined,
50
50
  onEvent: (event) => {
51
- const parts = [`trace ${event.name}`, `${event.ms}ms`];
52
- if (event.count !== undefined)
53
- parts.push(`count=${event.count}`);
54
- if (event.detail)
55
- parts.push(event.detail);
56
- console.error(parts.join(" "));
51
+ if (command.json)
52
+ return;
53
+ console.error(formatStep(event.name, event.ms, event.count, event.detail));
57
54
  },
58
55
  });
59
56
  const profile = await loadSearchProfile(command.searchProfilePath);
@@ -61,12 +58,12 @@ export async function runSearchCommand(command, startedAt) {
61
58
  const patternIds = checks.map((check) => check.id);
62
59
  const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
63
60
  const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
61
+ printRunPlan(command, patternIds);
64
62
  const { value: changeSet } = await t.trace("entity.diff", () => semChangeSetForCommand(command), {
65
63
  count: (v) => v.summary.total,
66
64
  detail: (v) => `${v.summary.fileCount} files`,
67
65
  });
68
66
  try {
69
- printRunPlan(command, changeSet.summary.fileCount, changeSet.summary.total, patternIds);
70
67
  const candidates = counterScoutTargets(changeSet, checks, maxCandidates);
71
68
  const contexts = entityContextsFromChanges(candidates, changeSet.changes);
72
69
  const targetsByPattern = countTargetsByPattern(contexts);
@@ -136,9 +133,38 @@ export async function runSearchCommand(command, startedAt) {
136
133
  const pack = profile?.context === "sem" || searchContexts.length === contexts.length
137
134
  ? initialPack
138
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
+ }
139
166
  const modelPath = await firstRunModelBootstrap(command.model);
140
167
  const model = await loadLocalModel(modelPath, command.model, "scout");
141
- const request = buildSearchRequest(changeSet, searchContexts, pack, checks, profile, command.includeCounterReasonInPrompt);
142
168
  const inputTokens = await countPromptTokens(model, request.prompt);
143
169
  if (inputTokens > maxSearchInputTokens) {
144
170
  return {
@@ -207,14 +233,36 @@ function buildSearchRequest(changeSet, contexts, pack, patterns, profile, includ
207
233
  includeCounterReasonInPrompt: profile?.includeCounterReasonInPrompt ?? includeCounterReasonInPrompt,
208
234
  });
209
235
  }
210
- function printRunPlan(command, filesChanged, entitiesScanned, patternIds) {
236
+ function printRunPlan(command, patternIds) {
211
237
  if (command.json)
212
238
  return;
213
239
  console.error("🧙 stupify 🪄");
214
- console.error(`Mode: search (${command.source})`);
215
- console.error(`Sem: ${filesChanged} files, ${entitiesScanned} changed entities`);
240
+ console.error(`Search: ${sourceLabel(command)}`);
216
241
  console.error(`Patterns: ${patternIds.join(", ")}`);
217
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
+ }
218
266
  function countTargetsByPattern(contexts) {
219
267
  const counts = {};
220
268
  for (const context of contexts)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stupify/cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Local-only diagnostic CLI for checking whether AI is making you dumber.",
5
5
  "private": false,
6
6
  "type": "module",
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
@@ -1,4 +1,4 @@
1
- export const VERSION = "0.0.5";
1
+ export const VERSION = "0.0.6";
2
2
  import type { ModelConfig, ModelId } from "./types.ts";
3
3
 
4
4
  export const DEFAULT_MODEL_ID: ModelId = "gemma-4-e2b";
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 already-loaded local ${profile} model: ${selectedModel.name}`,
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 ${runtime.profile} model server: ${modelName}`);
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
- `Restarting local ${runtime.profile} model server for selected model.`,
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
- stupify ${sourceHint(command)} --max-search-input-tokens ${Math.max((run.stats.inputTokens ?? 12_000) + 1, (run.stats.inputTokenCap ?? 12_000) * 2)}`;
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
@@ -58,10 +58,8 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
58
58
  const t = createTracer({
59
59
  writeLine: () => undefined,
60
60
  onEvent: (event) => {
61
- const parts = [`trace ${event.name}`, `${event.ms}ms`];
62
- if (event.count !== undefined) parts.push(`count=${event.count}`);
63
- if (event.detail) parts.push(event.detail);
64
- console.error(parts.join(" "));
61
+ if (command.json) return;
62
+ console.error(formatStep(event.name, event.ms, event.count, event.detail));
65
63
  },
66
64
  });
67
65
 
@@ -70,6 +68,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
70
68
  const patternIds = checks.map((check) => check.id);
71
69
  const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
72
70
  const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
71
+ printRunPlan(command, patternIds);
73
72
  const { value: changeSet } = await t.trace(
74
73
  "entity.diff",
75
74
  () => semChangeSetForCommand(command),
@@ -80,7 +79,6 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
80
79
  );
81
80
 
82
81
  try {
83
- printRunPlan(command, changeSet.summary.fileCount, changeSet.summary.total, patternIds);
84
82
  const candidates = counterScoutTargets(changeSet, checks, maxCandidates);
85
83
  const contexts = entityContextsFromChanges(candidates, changeSet.changes);
86
84
  const targetsByPattern = countTargetsByPattern(contexts);
@@ -156,8 +154,6 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
156
154
  ? initialPack
157
155
  : await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
158
156
 
159
- const modelPath = await firstRunModelBootstrap(command.model);
160
- const model = await loadLocalModel(modelPath, command.model, "scout");
161
157
  const request = buildSearchRequest(
162
158
  changeSet,
163
159
  searchContexts,
@@ -166,6 +162,38 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
166
162
  profile,
167
163
  command.includeCounterReasonInPrompt,
168
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");
169
197
  const inputTokens = await countPromptTokens(model, request.prompt);
170
198
  if (inputTokens > maxSearchInputTokens) {
171
199
  return {
@@ -250,17 +278,33 @@ function buildSearchRequest(
250
278
 
251
279
  function printRunPlan(
252
280
  command: SearchCommand,
253
- filesChanged: number,
254
- entitiesScanned: number,
255
281
  patternIds: readonly string[],
256
282
  ): void {
257
283
  if (command.json) return;
258
284
  console.error("🧙 stupify 🪄");
259
- console.error(`Mode: search (${command.source})`);
260
- console.error(`Sem: ${filesChanged} files, ${entitiesScanned} changed entities`);
285
+ console.error(`Search: ${sourceLabel(command)}`);
261
286
  console.error(`Patterns: ${patternIds.join(", ")}`);
262
287
  }
263
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
+
264
308
  function countTargetsByPattern(contexts: readonly SemContext[]): Record<string, number> {
265
309
  const counts: Record<string, number> = {};
266
310
  for (const context of contexts) counts[context.checkId] = (counts[context.checkId] ?? 0) + 1;