aish-cli 1.1.0 → 1.1.1

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/aish.plugin.zsh CHANGED
@@ -327,13 +327,19 @@ Respond with only the updated command."
327
327
  wait $pid 2>/dev/null
328
328
  local exit_code=$?
329
329
  cmd=$(<"$tmpfile")
330
- local errmsg=$(<"$errfile")
330
+ local stats=$(<"$errfile")
331
331
  rm -f "$tmpfile" "$errfile"
332
332
 
333
+ # Strip ANSI codes from stats for clean display
334
+ stats="${stats//$'\e[2m'/}"
335
+ stats="${stats//$'\e[0m'/}"
336
+ stats="${stats## }" # trim leading space
337
+ stats="${stats%%$'\n'}" # trim trailing newline
338
+
333
339
  if [[ $exit_code -ne 0 ]]; then
334
340
  # Show error briefly
335
341
  BUFFER=""
336
- POSTDISPLAY=$'\n '"Error: ${errmsg:-aish command failed}"
342
+ POSTDISPLAY=$'\n '"Error: ${stats:-aish command failed}"
337
343
  region_highlight=()
338
344
  zle -R
339
345
  sleep 2
@@ -369,10 +375,11 @@ Command: ${cmd}"
369
375
  Command: ${cmd}"
370
376
  fi
371
377
 
372
- # Show result with hints
378
+ # Show result with hints and stats
373
379
  BUFFER="$cmd"
374
380
  CURSOR=${#BUFFER}
375
381
  local hints=$'\n tab: refine │ enter: accept │ esc: cancel'
382
+ [[ -n "$stats" ]] && hints+=" │ ${stats}"
376
383
  POSTDISPLAY="$hints"
377
384
  # Highlight POSTDISPLAY area (starts after BUFFER)
378
385
  local hint_start=${#BUFFER}
package/dist/index.js CHANGED
@@ -8,56 +8,59 @@ import { tmpdir, homedir } from "os";
8
8
  import { join } from "path";
9
9
  var SYSTEM_PROMPT = `You are a CLI assistant that converts natural language into the exact shell commands to run.
10
10
 
11
- Some project files are provided below for reference so you don't need to read them yourself.
12
-
13
11
  GUIDELINES:
14
- 1. Review the project files provided below to understand available scripts and commands.
15
- 2. If you need more context, use the Read tool to look at relevant files (scripts, configs, docs).
16
- 3. Prefer existing scripts/targets from package.json, Makefile, etc. over raw commands.
17
- 4. If unsure about exact flags, use common/standard ones or provide multiple options.
12
+ 1. For standard commands (git, ls, curl, etc.), respond immediately without reading files.
13
+ 2. For project-specific commands (build, test, run scripts, etc.), ALWAYS read README.md first (if it exists), then the relevant config file:
14
+ - Node.js: package.json
15
+ - Python: pyproject.toml, setup.py, or Pipfile
16
+ - Rust: Cargo.toml
17
+ - Go: go.mod
18
+ - Ruby: Gemfile
19
+ - Make: Makefile
20
+ - Other: Justfile, Taskfile.yml
21
+ 3. ALWAYS prefer project scripts over direct tool invocation:
22
+ - Use "npm run build" not "tsup" or "tsc"
23
+ - Use "make test" not the underlying test command
24
+ - Use "cargo build" not "rustc" directly
25
+ This ensures correct flags, environment, and project configuration.
26
+ 4. If unsure about exact flags, provide multiple options.
18
27
 
19
28
  RESPONSE FORMAT \u2014 THIS IS CRITICAL:
20
29
  Your final message MUST be ONLY a JSON object. No prose, no explanation, no "Based on...", no markdown.
21
30
  Exactly this format: {"commands": ["command1"]}
22
31
  If you are unsure which command the user wants, return multiple options and the user will pick: {"commands": ["option1", "option2"]}
23
- Prefer existing scripts/targets with correct flags over raw commands.
24
32
  For values the user hasn't specified, use <placeholders> like: git commit -m "<message>" or curl <url>. Use descriptive names inside the angle brackets.`;
25
- var PROJECT_FILES = [
26
- "Makefile",
27
- "package.json",
28
- "README.md",
29
- "Justfile",
30
- "Taskfile.yml",
31
- "docker-compose.yml",
32
- "Cargo.toml",
33
- "pyproject.toml",
34
- "Gemfile"
33
+ var TOOL_HINTS = [
34
+ { files: ["bun.lockb", "bun.lock"], hint: "Use bun (not npm/yarn/pnpm)" },
35
+ { files: ["pnpm-lock.yaml"], hint: "Use pnpm (not npm/yarn)" },
36
+ { files: ["yarn.lock"], hint: "Use yarn (not npm/pnpm)" },
37
+ { files: ["package-lock.json"], hint: "Use npm (not yarn/pnpm)" },
38
+ { files: ["Cargo.lock"], hint: "Use cargo for Rust commands" },
39
+ { files: ["poetry.lock"], hint: "Use poetry (not pip)" },
40
+ { files: ["Pipfile.lock"], hint: "Use pipenv (not pip)" },
41
+ { files: ["uv.lock"], hint: "Use uv (not pip/poetry)" },
42
+ { files: ["Gemfile.lock"], hint: "Use bundle exec for Ruby commands" },
43
+ { files: ["go.sum"], hint: "Use go modules" },
44
+ { files: ["flake.lock"], hint: "Nix flake project - use nix commands" }
35
45
  ];
36
- var MAX_FILE_SIZE = 4e3;
37
46
  async function gatherProjectContext(cwd) {
38
- const sections = [];
39
- const found = [];
40
- for (const file of PROJECT_FILES) {
41
- const filePath = join(cwd, file);
42
- try {
43
- await access(filePath);
44
- let content = await readFile(filePath, "utf-8");
45
- if (content.length > MAX_FILE_SIZE) {
46
- content = content.slice(0, MAX_FILE_SIZE) + "\n...(truncated)";
47
+ const hints = [];
48
+ for (const { files, hint } of TOOL_HINTS) {
49
+ for (const file of files) {
50
+ try {
51
+ await access(join(cwd, file));
52
+ hints.push(hint);
53
+ break;
54
+ } catch {
47
55
  }
48
- sections.push(`--- ${file} ---
49
- ${content}`);
50
- found.push(file);
51
- } catch {
52
56
  }
53
57
  }
54
- if (found.length > 0) logVerbose(` context: ${found.join(", ")}`);
55
- if (sections.length === 0) return "";
58
+ if (hints.length > 0) logVerbose(` hints: ${hints.join(", ")}`);
59
+ if (hints.length === 0) return "";
56
60
  return `
57
61
 
58
- Project files:
59
-
60
- ${sections.join("\n\n")}`;
62
+ Tool hints:
63
+ - ${hints.join("\n- ")}`;
61
64
  }
62
65
  var verbose = false;
63
66
  function setVerbose(v) {
@@ -176,7 +179,7 @@ async function queryClaude(query, cwd, model) {
176
179
  const cached = await cacheGet(key);
177
180
  if (cached) {
178
181
  logVerbose(" cache: hit");
179
- return cached;
182
+ return { ...cached, stats: { cached: true } };
180
183
  }
181
184
  const prompt = `${SYSTEM_PROMPT}${context}
182
185
 
@@ -192,12 +195,19 @@ User request: ${query}`;
192
195
  ];
193
196
  const { stdout } = await spawnWithStdin("claude", args, prompt, cwd);
194
197
  let text = stdout.trim();
198
+ let stats = {};
195
199
  try {
196
200
  const wrapper = JSON.parse(text);
197
201
  if (verbose) {
198
202
  logVerbose(formatStats(wrapper));
199
203
  if (wrapper.result) logVerbose(` result: ${wrapper.result}`);
200
204
  }
205
+ stats.durationMs = wrapper.duration_ms;
206
+ stats.cost = wrapper.total_cost_usd;
207
+ if (wrapper.usage) {
208
+ stats.inputTokens = (wrapper.usage.input_tokens || 0) + (wrapper.usage.cache_creation_input_tokens || 0) + (wrapper.usage.cache_read_input_tokens || 0);
209
+ stats.outputTokens = wrapper.usage.output_tokens || 0;
210
+ }
201
211
  if (wrapper.result) {
202
212
  text = wrapper.result;
203
213
  }
@@ -206,9 +216,11 @@ User request: ${query}`;
206
216
  }
207
217
  const result = parseResponse(text);
208
218
  result.cacheKey = key;
219
+ result.stats = stats;
209
220
  return result;
210
221
  }
211
222
  async function queryCodex(query, cwd, model) {
223
+ const startTime = Date.now();
212
224
  const context = await gatherProjectContext(cwd);
213
225
  const resultFile = join(tmpdir(), `aish-codex-${Date.now()}.txt`);
214
226
  const prompt = `${SYSTEM_PROMPT}${context}
@@ -218,6 +230,8 @@ User request: ${query}`;
218
230
  "exec",
219
231
  "--sandbox",
220
232
  "read-only",
233
+ "-c",
234
+ 'model_reasoning_effort="low"',
221
235
  "-o",
222
236
  resultFile,
223
237
  "-"
@@ -230,7 +244,9 @@ User request: ${query}`;
230
244
  const text = await readFile(resultFile, "utf-8");
231
245
  await unlink(resultFile).catch(() => {
232
246
  });
233
- return parseResponse(text);
247
+ const result = parseResponse(text);
248
+ result.stats = { durationMs: Date.now() - startTime };
249
+ return result;
234
250
  }
235
251
  async function queryAi(provider, query, cwd, model) {
236
252
  if (provider === "codex") {
@@ -410,10 +426,12 @@ async function main() {
410
426
  } : startSpinner("Thinking...");
411
427
  let commands;
412
428
  let resultCacheKey;
429
+ let stats;
413
430
  try {
414
431
  const result = await queryAi(provider, query, cwd, model);
415
432
  commands = result.commands;
416
433
  resultCacheKey = result.cacheKey;
434
+ stats = result.stats;
417
435
  } catch (err) {
418
436
  stopSpinner();
419
437
  if (print) {
@@ -423,6 +441,23 @@ async function main() {
423
441
  process.exit(1);
424
442
  }
425
443
  stopSpinner();
444
+ if (stats) {
445
+ const parts = [];
446
+ if (stats.cached) {
447
+ parts.push("cached");
448
+ } else {
449
+ if (stats.durationMs) parts.push(`${(stats.durationMs / 1e3).toFixed(1)}s`);
450
+ if (stats.inputTokens || stats.outputTokens) {
451
+ parts.push(`${stats.inputTokens || 0}\u2192${stats.outputTokens || 0} tok`);
452
+ }
453
+ if (stats.cost) parts.push(`$${stats.cost.toFixed(4)}`);
454
+ }
455
+ if (parts.length > 0) {
456
+ const output = print ? process.stderr : process.stdout;
457
+ output.write(`${DIM3} ${parts.join(" \xB7 ")}${RESET2}
458
+ `);
459
+ }
460
+ }
426
461
  if (commands.length === 0) {
427
462
  if (print) {
428
463
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aish-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "AI Shell - convert natural language to bash commands using Claude or Codex",
5
5
  "type": "module",
6
6
  "license": "MIT",