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.
- package/README.md +67 -0
- package/dist/index.js +280 -0
- 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
|
+
}
|