@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 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.4";
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.4";
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
@@ -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
- const parts = [`trace ${event.name}`, `${event.ms}ms`];
51
- if (event.count !== undefined)
52
- parts.push(`count=${event.count}`);
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, filesChanged, entitiesScanned, patternIds) {
236
+ function printRunPlan(command, patternIds) {
210
237
  if (command.json)
211
238
  return;
212
239
  console.error("🧙 stupify 🪄");
213
- console.error(`Mode: search (${command.source})`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stupify/cli",
3
- "version": "0.0.4",
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.4";
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
@@ -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
- const parts = [`trace ${event.name}`, `${event.ms}ms`];
61
- if (event.count !== undefined) parts.push(`count=${event.count}`);
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(`Mode: search (${command.source})`);
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
  }