aish-cli 1.0.0

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.
Files changed (3) hide show
  1. package/README.md +67 -0
  2. package/dist/index.js +280 -0
  3. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # aish
2
+
3
+ AI Shell - convert natural language to bash commands.
4
+
5
+ Describe what you want to do and `aish` translates it into the right shell command using [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [Codex](https://github.com/openai/codex) CLI. It reads your project files (Makefile, package.json, README, etc.) and checks `--help` output to get the exact flags right.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g aish-cli
11
+ ```
12
+
13
+ Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` CLI) or [Codex](https://github.com/openai/codex) installed and authenticated.
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ aish <natural language query>
19
+ ```
20
+
21
+ ### Examples
22
+
23
+ ```bash
24
+ # Simple commands
25
+ aish list files
26
+
27
+ # Project-aware - reads your Makefile/package.json to find the right command
28
+ aish start the dev server
29
+ aish package ios app in debug, skip setup
30
+
31
+ # Multi-step tasks
32
+ aish create a new branch, commit everything, and push
33
+ ```
34
+
35
+ `aish` will suggest a command, then let you **Run**, **Edit**, or **Cancel** before executing anything.
36
+
37
+ ### Options
38
+
39
+ ```
40
+ -p, --provider <claude|codex> AI provider (default: claude)
41
+ -m, --model <model> Model override
42
+ --cwd <dir> Working directory
43
+ -v, --verbose Show debug output
44
+ -h, --help Show help
45
+ ```
46
+
47
+ ### Environment Variables
48
+
49
+ | Variable | Description | Default |
50
+ |---|---|---|
51
+ | `AISH_PROVIDER` | AI provider (`claude` or `codex`) | `claude` |
52
+ | `AISH_MODEL` | Model to use | `sonnet` |
53
+
54
+ Flags take precedence over environment variables.
55
+
56
+ ## How It Works
57
+
58
+ 1. You type a natural language query
59
+ 2. `aish` invokes the AI CLI in your project directory
60
+ 3. The AI reads your project files (README, Makefile, package.json, etc.) and runs `--help` on relevant commands to discover exact flags
61
+ 4. Returns one or more suggested commands
62
+ 5. You choose to **Run**, **Edit**, or **Cancel**
63
+ 6. On Run/Edit the command executes with your shell, inheriting stdio
64
+
65
+ ## License
66
+
67
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/ai.ts
4
+ import { execFile, spawn } from "child_process";
5
+ import { readFile, unlink } from "fs/promises";
6
+ import { tmpdir } from "os";
7
+ import { join } from "path";
8
+ var SYSTEM_PROMPT = `You are a CLI assistant that converts natural language into the exact shell commands to run.
9
+
10
+ RESEARCH STEPS (do all of these before responding):
11
+ 1. Read the README.md (or README) file thoroughly \u2014 it often documents available commands, flags, and workflows.
12
+ 2. Read the Makefile, package.json scripts, Justfile, Taskfile.yml, docker-compose.yml, Cargo.toml, or pyproject.toml \u2014 whichever exist \u2014 to find available targets/scripts.
13
+ 3. When the user's request maps to a specific command or script, run it with --help to discover the exact flags and options available.
14
+ 4. Cross-reference what you found: use the exact flag names and syntax from --help output and documentation, not guesses.
15
+
16
+ RESPONSE FORMAT:
17
+ Respond with ONLY a JSON object: {"commands": ["command1", "command2"]}
18
+ No explanation, no markdown, no code fences. Just raw JSON.
19
+ Prefer existing scripts/targets with correct flags over raw commands.`;
20
+ var verbose = false;
21
+ function setVerbose(v) {
22
+ verbose = v;
23
+ }
24
+ function execPromise(cmd, args, cwd) {
25
+ if (verbose) {
26
+ console.error(`\x1B[2m$ ${cmd} ${args.join(" ")}\x1B[0m`);
27
+ console.error(`\x1B[2m cwd: ${cwd}\x1B[0m`);
28
+ }
29
+ return new Promise((resolve, reject) => {
30
+ execFile(cmd, args, { cwd, maxBuffer: 1024 * 1024, timeout: 12e4 }, (err, stdout, stderr) => {
31
+ if (verbose) {
32
+ if (stderr) console.error(`\x1B[2mstderr: ${stderr}\x1B[0m`);
33
+ if (stdout) console.error(`\x1B[2mstdout: ${stdout.slice(0, 500)}\x1B[0m`);
34
+ if (err) console.error(`\x1B[2merror: ${err.message}\x1B[0m`);
35
+ }
36
+ if (err) reject(err);
37
+ else resolve({ stdout, stderr });
38
+ });
39
+ });
40
+ }
41
+ function parseCommands(raw) {
42
+ let cleaned = raw.trim();
43
+ cleaned = cleaned.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "");
44
+ cleaned = cleaned.trim();
45
+ const parsed = JSON.parse(cleaned);
46
+ if (!parsed.commands || !Array.isArray(parsed.commands)) {
47
+ throw new Error("Invalid response: missing commands array");
48
+ }
49
+ if (!parsed.commands.every((c) => typeof c === "string")) {
50
+ throw new Error("Invalid response: commands must be strings");
51
+ }
52
+ return parsed.commands;
53
+ }
54
+ function spawnWithStdin(cmd, args, input2, cwd) {
55
+ if (verbose) {
56
+ console.error(`\x1B[2m$ echo '...' | ${cmd} ${args.join(" ")}\x1B[0m`);
57
+ console.error(`\x1B[2m cwd: ${cwd}\x1B[0m`);
58
+ console.error(`\x1B[2m stdin: ${input2.slice(0, 200)}...\x1B[0m`);
59
+ }
60
+ return new Promise((resolve, reject) => {
61
+ const child = spawn(cmd, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
62
+ let stdout = "";
63
+ let stderr = "";
64
+ child.stdout.on("data", (d) => stdout += d);
65
+ child.stderr.on("data", (d) => stderr += d);
66
+ child.on("close", (code) => {
67
+ if (verbose) {
68
+ if (stderr) console.error(`\x1B[2mstderr: ${stderr.slice(0, 500)}\x1B[0m`);
69
+ if (stdout) console.error(`\x1B[2mstdout: ${stdout.slice(0, 500)}\x1B[0m`);
70
+ }
71
+ if (code !== 0) reject(new Error(`${cmd} exited with code ${code}
72
+ ${stderr}`));
73
+ else resolve({ stdout, stderr });
74
+ });
75
+ child.on("error", reject);
76
+ child.stdin.write(input2);
77
+ child.stdin.end();
78
+ });
79
+ }
80
+ async function queryClaude(query, cwd, model) {
81
+ const prompt = `${SYSTEM_PROMPT}
82
+
83
+ User request: ${query}`;
84
+ const args = [
85
+ "-p",
86
+ "--output-format",
87
+ "json"
88
+ ];
89
+ if (model) {
90
+ args.push("--model", model);
91
+ } else {
92
+ args.push("--model", "sonnet");
93
+ }
94
+ const { stdout } = await spawnWithStdin("claude", args, prompt, cwd);
95
+ let text = stdout.trim();
96
+ try {
97
+ const wrapper = JSON.parse(text);
98
+ if (wrapper.result) {
99
+ text = wrapper.result;
100
+ }
101
+ } catch {
102
+ }
103
+ return { commands: parseCommands(text) };
104
+ }
105
+ async function queryCodex(query, cwd, model) {
106
+ const resultFile = join(tmpdir(), `aish-codex-${Date.now()}.txt`);
107
+ const args = [
108
+ "exec",
109
+ "-o",
110
+ resultFile,
111
+ `${SYSTEM_PROMPT}
112
+
113
+ User request: ${query}`
114
+ ];
115
+ if (model) {
116
+ args.push("--model", model);
117
+ }
118
+ await execPromise("codex", args, cwd);
119
+ const text = await readFile(resultFile, "utf-8");
120
+ await unlink(resultFile).catch(() => {
121
+ });
122
+ return { commands: parseCommands(text) };
123
+ }
124
+ async function queryAi(provider, query, cwd, model) {
125
+ if (provider === "codex") {
126
+ return queryCodex(query, cwd, model);
127
+ }
128
+ return queryClaude(query, cwd, model);
129
+ }
130
+
131
+ // src/ui.ts
132
+ import { select, input } from "@inquirer/prompts";
133
+ var CYAN = "\x1B[36m";
134
+ var RESET = "\x1B[0m";
135
+ async function promptEdit(cmd) {
136
+ const edited = await input({
137
+ message: "Edit command:",
138
+ default: cmd
139
+ });
140
+ const trimmed = edited.trim();
141
+ return trimmed || null;
142
+ }
143
+ async function promptAction(cmd) {
144
+ console.log(`
145
+ ${CYAN}${cmd}${RESET}
146
+ `);
147
+ const action = await select({
148
+ message: "Action:",
149
+ choices: [
150
+ { name: "Run", value: "run" },
151
+ { name: "Edit", value: "edit" },
152
+ { name: "Cancel", value: "cancel" }
153
+ ]
154
+ });
155
+ if (action === "run") return cmd;
156
+ if (action === "edit") return promptEdit(cmd);
157
+ return null;
158
+ }
159
+ async function promptCommand(commands) {
160
+ if (commands.length === 1) {
161
+ return promptAction(commands[0]);
162
+ }
163
+ const choice = await select({
164
+ message: "Select a command to run:",
165
+ choices: [
166
+ ...commands.map((cmd) => ({
167
+ name: `${CYAN}${cmd}${RESET}`,
168
+ value: cmd
169
+ })),
170
+ { name: "Cancel", value: "__cancel__" }
171
+ ]
172
+ });
173
+ if (choice === "__cancel__") return null;
174
+ return promptAction(choice);
175
+ }
176
+
177
+ // src/exec.ts
178
+ import { spawn as spawn2 } from "child_process";
179
+ function execCommand(cmd, cwd) {
180
+ return new Promise((resolve) => {
181
+ const shell = process.env.SHELL || "/bin/bash";
182
+ const child = spawn2(shell, ["-c", cmd], {
183
+ stdio: "inherit",
184
+ cwd
185
+ });
186
+ child.on("close", (code) => resolve(code ?? 1));
187
+ child.on("error", () => resolve(1));
188
+ });
189
+ }
190
+
191
+ // src/index.ts
192
+ var BRAILLE = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
193
+ var DIM = "\x1B[2m";
194
+ var RESET2 = "\x1B[0m";
195
+ var RED = "\x1B[31m";
196
+ function startSpinner(message) {
197
+ let i = 0;
198
+ const interval = setInterval(() => {
199
+ process.stderr.write(`\r${DIM}${BRAILLE[i++ % BRAILLE.length]} ${message}${RESET2}`);
200
+ }, 80);
201
+ return () => {
202
+ clearInterval(interval);
203
+ process.stderr.write("\r\x1B[K");
204
+ };
205
+ }
206
+ function parseArgs(argv) {
207
+ const args = argv.slice(2);
208
+ let provider = process.env.AISH_PROVIDER || "claude";
209
+ let cwd = process.cwd();
210
+ let model = process.env.AISH_MODEL || void 0;
211
+ let verbose2 = false;
212
+ const queryParts = [];
213
+ for (let i = 0; i < args.length; i++) {
214
+ const arg = args[i];
215
+ if (arg === "-p" || arg === "--provider") {
216
+ const val = args[++i];
217
+ if (val !== "claude" && val !== "codex") {
218
+ console.error(`${RED}Invalid provider: ${val}. Use "claude" or "codex".${RESET2}`);
219
+ process.exit(1);
220
+ }
221
+ provider = val;
222
+ } else if (arg === "--cwd") {
223
+ cwd = args[++i];
224
+ } else if (arg === "-m" || arg === "--model") {
225
+ model = args[++i];
226
+ } else if (arg === "-v" || arg === "--verbose") {
227
+ verbose2 = true;
228
+ } else if (arg === "-h" || arg === "--help") {
229
+ console.log(`Usage: aish [options] <query...>
230
+
231
+ Options:
232
+ -p, --provider <claude|codex> AI provider (default: claude, env: AISH_PROVIDER)
233
+ -m, --model <model> Model override (env: AISH_MODEL)
234
+ --cwd <dir> Working directory
235
+ -v, --verbose Show debug output
236
+ -h, --help Show help`);
237
+ process.exit(0);
238
+ } else {
239
+ queryParts.push(arg);
240
+ }
241
+ }
242
+ return { query: queryParts.join(" "), provider, cwd, model, verbose: verbose2 };
243
+ }
244
+ async function main() {
245
+ const { query, provider, cwd, model, verbose: verbose2 } = parseArgs(process.argv);
246
+ setVerbose(verbose2);
247
+ if (!query) {
248
+ console.error(`${RED}Usage: aish <query>${RESET2}`);
249
+ process.exit(1);
250
+ }
251
+ const stopSpinner = verbose2 ? () => {
252
+ } : startSpinner("Thinking...");
253
+ let commands;
254
+ try {
255
+ const result = await queryAi(provider, query, cwd, model);
256
+ commands = result.commands;
257
+ } catch (err) {
258
+ stopSpinner();
259
+ console.error(`${RED}Error: ${err.message}${RESET2}`);
260
+ process.exit(1);
261
+ }
262
+ stopSpinner();
263
+ if (commands.length === 0) {
264
+ console.error(`${RED}No commands suggested.${RESET2}`);
265
+ process.exit(1);
266
+ }
267
+ let chosen;
268
+ try {
269
+ chosen = await promptCommand(commands);
270
+ } catch {
271
+ console.log();
272
+ process.exit(130);
273
+ }
274
+ if (!chosen) {
275
+ process.exit(0);
276
+ }
277
+ const exitCode = await execCommand(chosen, cwd);
278
+ process.exit(exitCode);
279
+ }
280
+ main();
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "aish-cli",
3
+ "version": "1.0.0",
4
+ "description": "AI Shell - convert natural language to bash commands using Claude or Codex",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/janicduplessis/aish.git"
10
+ },
11
+ "keywords": ["ai", "shell", "cli", "claude", "codex", "bash", "natural-language"],
12
+ "bin": {
13
+ "aish": "./dist/index.js"
14
+ },
15
+ "files": ["dist"],
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "dev": "tsx src/index.ts",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "@inquirer/prompts": "^7.5.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.15.0",
26
+ "tsup": "^8.4.0",
27
+ "tsx": "^4.19.0",
28
+ "typescript": "^5.8.0"
29
+ }
30
+ }