croo-cli 0.1.1 → 0.1.2
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 +94 -34
- package/dist/cli.js +85 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,52 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
▄▄▄ ▄▄▄
|
|
5
|
+
██▄█▄▄▄█▄██
|
|
6
|
+
▀█▄▀▀▀▀▀▄█▀
|
|
7
|
+
▀▀ ▀▀
|
|
8
|
+
```
|
|
9
|
+
|
|
1
10
|
# Croo
|
|
2
11
|
|
|
3
|
-
A
|
|
12
|
+
**A tiny terminal coding agent with a frog in it.**
|
|
13
|
+
Bring your own API key, or a local model. No accounts, no telemetry, no backend.
|
|
14
|
+
|
|
15
|
+
[](https://www.npmjs.com/package/croo-cli)
|
|
16
|
+
[](https://github.com/xandertaylorx/croo/actions/workflows/ci.yml)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
[](https://nodejs.org)
|
|
19
|
+
[](package.json)
|
|
20
|
+
|
|
21
|
+
</div>
|
|
4
22
|
|
|
5
23
|
```
|
|
6
24
|
▄▄▄ ▄▄▄ “ribbit. ready when you are.”
|
|
7
|
-
██▄█▄▄▄█▄██ Croo v0.1.
|
|
25
|
+
██▄█▄▄▄█▄██ Croo v0.1.1
|
|
8
26
|
▀█▄▀▀▀▀▀▄█▀ gpt-4o · openai
|
|
9
27
|
▀▀ ▀▀ ~/projects/my-app
|
|
10
28
|
/help for commands
|
|
29
|
+
|
|
30
|
+
gpt-4o · ~/projects/my-app
|
|
31
|
+
› add a --verbose flag to the CLI
|
|
32
|
+
|
|
33
|
+
◇ read_file src/cli.ts ✓
|
|
34
|
+
✎ edit_file src/cli.ts
|
|
35
|
+
╭─ edit_file src/cli.ts ─────────────────────────
|
|
36
|
+
│ 12 const args = parseArgs(process.argv)
|
|
37
|
+
│ 13 + const verbose = args.verbose
|
|
38
|
+
╰ Allow? [y]es / [n]o / [a]lways
|
|
11
39
|
```
|
|
12
40
|
|
|
13
|
-
|
|
14
|
-
- **Provider-agnostic.** OpenAI-compatible endpoints, Anthropic, and Google Gemini.
|
|
15
|
-
- **Tiny.** Zero runtime dependencies: native `fetch` and Node built-ins only.
|
|
41
|
+
## Why Croo
|
|
16
42
|
|
|
17
|
-
|
|
43
|
+
- **Fully local.** Your config and history stay on your machine, in `~/.config/croo` and `~/.local/share/croo`.
|
|
44
|
+
- **Bring your own model.** OpenAI, Anthropic, Google Gemini, or anything OpenAI-compatible: OpenRouter, Groq, Together, DeepSeek, Mistral, Ollama, LM Studio, llama.cpp, vLLM.
|
|
45
|
+
- **Small and readable.** TypeScript, zero runtime dependencies, native `fetch`. You can read the whole thing in an afternoon.
|
|
46
|
+
- **You stay in control.** Every file write and shell command shows a diff or the exact command and waits for your OK.
|
|
47
|
+
- **A little charm.** A pixel frog lives in your terminal. It blinks, hops while tools run, and reacts when things go well or badly. `--no-pet` sends it home.
|
|
48
|
+
|
|
49
|
+
> Croo works with API keys and any OpenAI-compatible endpoint, including local models. It does not use anyone's consumer-subscription logins.
|
|
18
50
|
|
|
19
51
|
## Install
|
|
20
52
|
|
|
@@ -22,27 +54,41 @@ Croo works with API keys and any OpenAI-compatible endpoint, including local mod
|
|
|
22
54
|
npm i -g croo-cli
|
|
23
55
|
```
|
|
24
56
|
|
|
25
|
-
|
|
57
|
+
Needs Node 20 or newer. The command is `croo`.
|
|
58
|
+
|
|
59
|
+
## Try it without an API key
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
croo --demo
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A scripted fake model walks you through the interface: streaming markdown, tool cards, the spinner, and a diff permission prompt. Nothing leaves your machine and nothing is saved.
|
|
26
66
|
|
|
27
67
|
## Quick start
|
|
28
68
|
|
|
29
69
|
```bash
|
|
30
70
|
cd your-project
|
|
31
|
-
croo
|
|
32
|
-
croo "
|
|
33
|
-
croo --continue
|
|
34
|
-
croo --profile local
|
|
35
|
-
croo --yolo # skip permission prompts (be careful)
|
|
36
|
-
croo --no-pet # no frog
|
|
37
|
-
croo --demo # look around with a fake model, no API key
|
|
71
|
+
croo # first run walks you through setup
|
|
72
|
+
croo "fix the failing test" # one-shot: run a task and exit
|
|
73
|
+
croo --continue # resume the last session in this directory
|
|
74
|
+
croo --profile local # use a named profile
|
|
38
75
|
```
|
|
39
76
|
|
|
40
|
-
|
|
41
|
-
|
|
77
|
+
| Flag | What it does |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `-p, --profile <name>` | Use a named profile from the config |
|
|
80
|
+
| `-c, --continue` | Resume the last session in this directory |
|
|
81
|
+
| `--yolo` | Skip permission prompts (be careful) |
|
|
82
|
+
| `--no-pet` | Hide the frog |
|
|
83
|
+
| `--demo` | Try the interface with a fake model |
|
|
84
|
+
| `-v, --version` / `-h, --help` | Version / help |
|
|
85
|
+
|
|
86
|
+
**In the session:** `/help` · `/model` (list; `/model <name>` switches; `/model add`) · `/clear` · `/config` · `/exit`.
|
|
87
|
+
`Ctrl+C` cancels the current generation. A second `Ctrl+C` exits.
|
|
42
88
|
|
|
43
|
-
##
|
|
89
|
+
## Configure providers
|
|
44
90
|
|
|
45
|
-
`~/.config/croo/config.json`
|
|
91
|
+
Config lives at `~/.config/croo/config.json` and is created with mode `600`. Add as many named profiles as you like and switch with `/model` or `--profile`.
|
|
46
92
|
|
|
47
93
|
```jsonc
|
|
48
94
|
{
|
|
@@ -50,9 +96,9 @@ Slash commands: `/help`, `/model` (list, `/model <name>` to switch, `/model add`
|
|
|
50
96
|
"pet": true,
|
|
51
97
|
"animations": true,
|
|
52
98
|
"profiles": {
|
|
53
|
-
"openai":
|
|
99
|
+
"openai": { "type": "openai", "model": "gpt-4o" },
|
|
54
100
|
"anthropic": { "type": "anthropic", "model": "claude-sonnet-4-5" },
|
|
55
|
-
"gemini":
|
|
101
|
+
"gemini": { "type": "gemini", "model": "gemini-2.5-pro" },
|
|
56
102
|
"openrouter": {
|
|
57
103
|
"type": "openai",
|
|
58
104
|
"baseUrl": "https://openrouter.ai/api/v1",
|
|
@@ -67,6 +113,8 @@ Slash commands: `/help`, `/model` (list, `/model <name>` to switch, `/model add`
|
|
|
67
113
|
}
|
|
68
114
|
```
|
|
69
115
|
|
|
116
|
+
**API keys** can go in the profile (`"apiKey": "..."`) or in the environment. Lookup order: the profile's `apiKey`, then `CROO_API_KEY`, then the provider's own variable (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`). Local servers like Ollama need no key.
|
|
117
|
+
|
|
70
118
|
Optional per-profile `inputPerMTok` / `outputPerMTok` (USD per million tokens) adds a cost estimate to the status line.
|
|
71
119
|
|
|
72
120
|
## Tools and permissions
|
|
@@ -74,36 +122,48 @@ Optional per-profile `inputPerMTok` / `outputPerMTok` (USD per million tokens) a
|
|
|
74
122
|
| Tool | What it does | Asks first? |
|
|
75
123
|
| --- | --- | --- |
|
|
76
124
|
| `read_file` | Read a file with line numbers | no |
|
|
77
|
-
| `list_files` | List
|
|
125
|
+
| `list_files` | List or glob files (respects `.gitignore`) | no |
|
|
78
126
|
| `grep` | Regex search (respects `.gitignore`) | no |
|
|
79
127
|
| `write_file` | Create or overwrite a file | yes, shows a diff |
|
|
80
128
|
| `edit_file` | Exact string replace | yes, shows a diff |
|
|
81
|
-
| `bash` | Run a shell command (120s timeout, truncated
|
|
129
|
+
| `bash` | Run a shell command (120s timeout, output truncated) | yes, shows the command |
|
|
130
|
+
|
|
131
|
+
Answer `y`, `n`, or `a` (always allow that tool for this session). File tools can never leave the current directory, symlinks included.
|
|
132
|
+
|
|
133
|
+
**Project instructions:** drop a `CROO.md` or `AGENTS.md` in your project and Croo adds it to its system prompt.
|
|
82
134
|
|
|
83
|
-
|
|
135
|
+
**Sessions** are saved as plain JSON in `~/.local/share/croo/sessions`.
|
|
84
136
|
|
|
85
|
-
|
|
137
|
+
## Look and feel
|
|
138
|
+
|
|
139
|
+
Colours come from one file, `src/ui/theme.ts`. Croo uses 24-bit colour when your terminal supports it and falls back to 256 or 16. It respects `NO_COLOR`, and turns off colour and animation when output is piped, so `croo "..." > out.txt` stays clean. Set `"animations": false` or `"pet": false` in the config to calm things down.
|
|
86
140
|
|
|
87
141
|
## Development
|
|
88
142
|
|
|
89
143
|
```bash
|
|
90
|
-
|
|
144
|
+
git clone https://github.com/xandertaylorx/croo && cd croo
|
|
145
|
+
npm install
|
|
146
|
+
npm test # vitest
|
|
147
|
+
npm run build # dist/cli.js
|
|
148
|
+
node dist/cli.js --demo
|
|
91
149
|
```
|
|
92
150
|
|
|
93
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
151
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and pull requests are welcome.
|
|
94
152
|
|
|
95
153
|
## Roadmap
|
|
96
154
|
|
|
97
|
-
- Custom pets: the `Pet` interface in `src/ui/pet.ts` is
|
|
98
|
-
- Idle animation at the prompt
|
|
99
|
-
- Context
|
|
100
|
-
- MCP support
|
|
101
|
-
- More tools
|
|
155
|
+
- [ ] Custom pets: the `Pet` interface in `src/ui/pet.ts` is built so you can drop in your own frames
|
|
156
|
+
- [ ] Idle animation at the prompt
|
|
157
|
+
- [ ] Context-window management and compaction
|
|
158
|
+
- [ ] MCP support
|
|
159
|
+
- [ ] More tools: web fetch, multi-edit
|
|
102
160
|
|
|
103
161
|
## License
|
|
104
162
|
|
|
105
|
-
MIT,
|
|
163
|
+
[MIT](LICENSE), Copyright (c) 2026 Xander Taylor. Free to use, change and share.
|
|
164
|
+
|
|
165
|
+
<div align="center">
|
|
106
166
|
|
|
107
|
-
|
|
167
|
+
<sub>An Entity of The Tizzle Collective</sub>
|
|
108
168
|
|
|
109
|
-
|
|
169
|
+
</div>
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import readline from "readline";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "croo-cli",
|
|
9
|
-
version: "0.1.
|
|
9
|
+
version: "0.1.2",
|
|
10
10
|
description: "Croo: a quick, minimal, open-source terminal AI coding agent. Bring your own API key.",
|
|
11
11
|
type: "module",
|
|
12
12
|
bin: {
|
|
@@ -937,6 +937,20 @@ function createProvider(profile, env = process.env) {
|
|
|
937
937
|
return openaiProvider(profile, key);
|
|
938
938
|
}
|
|
939
939
|
|
|
940
|
+
// src/providers/presets.ts
|
|
941
|
+
var PRESETS = [
|
|
942
|
+
{ id: "openai", label: "OpenAI", type: "openai", model: "gpt-4o" },
|
|
943
|
+
{ id: "anthropic", label: "Anthropic (Claude)", type: "anthropic", model: "claude-sonnet-4-5" },
|
|
944
|
+
{ id: "gemini", label: "Google Gemini", type: "gemini", model: "gemini-2.5-pro" },
|
|
945
|
+
{ id: "groq", label: "Groq", type: "openai", baseUrl: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile" },
|
|
946
|
+
{ id: "openrouter", label: "OpenRouter", type: "openai", baseUrl: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4.5" },
|
|
947
|
+
{ id: "deepseek", label: "DeepSeek", type: "openai", baseUrl: "https://api.deepseek.com/v1", model: "deepseek-chat" },
|
|
948
|
+
{ id: "xai", label: "xAI (Grok)", type: "openai", baseUrl: "https://api.x.ai/v1", model: "grok-4" },
|
|
949
|
+
{ id: "mistral", label: "Mistral", type: "openai", baseUrl: "https://api.mistral.ai/v1", model: "mistral-large-latest" },
|
|
950
|
+
{ id: "together", label: "Together AI", type: "openai", baseUrl: "https://api.together.xyz/v1", model: "meta-llama/Llama-3.3-70B-Instruct-Turbo" },
|
|
951
|
+
{ id: "ollama", label: "Ollama (local, no key)", type: "openai", baseUrl: "http://localhost:11434/v1", model: "llama3.2", noKey: true }
|
|
952
|
+
];
|
|
953
|
+
|
|
940
954
|
// src/session.ts
|
|
941
955
|
import fs5 from "fs";
|
|
942
956
|
import path5 from "path";
|
|
@@ -1530,27 +1544,63 @@ var ask = (question, opts) => {
|
|
|
1530
1544
|
muted = !!opts?.secret;
|
|
1531
1545
|
});
|
|
1532
1546
|
};
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
}
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1547
|
+
async function askKey(a, label) {
|
|
1548
|
+
for (; ; ) {
|
|
1549
|
+
const key = (await a(`Paste your ${label} API key: `, { secret: true })).trim();
|
|
1550
|
+
if (key) return key;
|
|
1551
|
+
if (!process.stdin.isTTY) return "";
|
|
1552
|
+
console.log(theme.muted(" (nothing pasted, try again)"));
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
async function setupProfile(a, cfg, opts = {}) {
|
|
1556
|
+
console.log(theme.bold("\nPick a provider\n"));
|
|
1557
|
+
PRESETS.forEach((p, i) => console.log(` ${String(i + 1).padStart(2)}) ${p.label}`));
|
|
1558
|
+
const otherN = PRESETS.length + 1;
|
|
1559
|
+
console.log(` ${String(otherN).padStart(2)}) Other (any OpenAI-compatible URL)`);
|
|
1560
|
+
if (opts.allowDemo) console.log(` ${String(otherN + 1).padStart(2)}) Demo mode (no key)`);
|
|
1561
|
+
console.log();
|
|
1562
|
+
let preset;
|
|
1563
|
+
let custom = false;
|
|
1564
|
+
for (; ; ) {
|
|
1565
|
+
const raw = (await a("Number: ")).trim().toLowerCase();
|
|
1566
|
+
if (!raw && !process.stdin.isTTY) return null;
|
|
1567
|
+
const n = Number(raw);
|
|
1568
|
+
preset = Number.isInteger(n) ? PRESETS[n - 1] : PRESETS.find((p) => p.id === raw);
|
|
1569
|
+
if (preset) break;
|
|
1570
|
+
if (n === otherN) {
|
|
1571
|
+
custom = true;
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
if (opts.allowDemo && (n === otherN + 1 || raw.startsWith("d"))) return null;
|
|
1575
|
+
console.log(theme.muted(" Pick a number from the list."));
|
|
1576
|
+
}
|
|
1577
|
+
let profile;
|
|
1578
|
+
let base;
|
|
1579
|
+
if (custom) {
|
|
1580
|
+
let url = "";
|
|
1581
|
+
while (!url) {
|
|
1582
|
+
url = (await a("Base URL (e.g. https://api.example.com/v1): ")).trim();
|
|
1583
|
+
try {
|
|
1584
|
+
new URL(url);
|
|
1585
|
+
} catch {
|
|
1586
|
+
url = "";
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
profile = { type: "openai", baseUrl: url, model: (await a("Model name: ")).trim() || "gpt-4o" };
|
|
1590
|
+
if (!isLocalUrl(url)) profile.apiKey = await askKey(a, new URL(url).hostname);
|
|
1591
|
+
base = new URL(url).hostname.replace(/^api\./, "").split(".")[0];
|
|
1592
|
+
} else {
|
|
1593
|
+
profile = { type: preset.type, model: preset.model };
|
|
1594
|
+
if (preset.baseUrl) profile.baseUrl = preset.baseUrl;
|
|
1595
|
+
if (!preset.noKey) profile.apiKey = await askKey(a, preset.label);
|
|
1596
|
+
base = preset.id;
|
|
1597
|
+
}
|
|
1548
1598
|
let name = base;
|
|
1549
1599
|
for (let i = 2; cfg.profiles[name]; i++) name = `${base}${i}`;
|
|
1550
1600
|
const next = { ...cfg, profiles: { ...cfg.profiles, [name]: profile }, activeProfile: name };
|
|
1551
1601
|
saveConfig(next);
|
|
1552
1602
|
console.log(theme.accent(`
|
|
1553
|
-
Saved
|
|
1603
|
+
Saved. Using ${profile.model}. Change it any time with /model.
|
|
1554
1604
|
`));
|
|
1555
1605
|
return next;
|
|
1556
1606
|
}
|
|
@@ -1565,13 +1615,6 @@ async function main() {
|
|
|
1565
1615
|
if (args.help) return console.log(HELP);
|
|
1566
1616
|
if (args.version) return console.log(VERSION);
|
|
1567
1617
|
let cfg = args.demo ? null : loadConfig();
|
|
1568
|
-
if (!args.demo && (!cfg || !Object.keys(cfg.profiles).length)) {
|
|
1569
|
-
if (process.stdin.isTTY) {
|
|
1570
|
-
console.log(theme.accent(theme.bold("Welcome to Croo.")) + " No config yet.");
|
|
1571
|
-
const choice = await ask("Set up a provider now, or look around in demo mode (no API key)? [setup/demo]: ");
|
|
1572
|
-
if (choice.trim().toLowerCase().startsWith("d")) args.demo = true;
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
1618
|
if (args.demo) {
|
|
1576
1619
|
cfg = { ...defaultConfig(), activeProfile: "demo", profiles: { demo: DEMO_PROFILE } };
|
|
1577
1620
|
} else if (!cfg || !Object.keys(cfg.profiles).length) {
|
|
@@ -1579,13 +1622,29 @@ async function main() {
|
|
|
1579
1622
|
console.error(`No config found. Run \`croo\` in a terminal to set one up, or create ${configPath()}.`);
|
|
1580
1623
|
process.exit(1);
|
|
1581
1624
|
}
|
|
1582
|
-
|
|
1625
|
+
console.log(theme.accent(theme.bold("Welcome to Croo.")));
|
|
1626
|
+
const made = await setupProfile(ask, cfg ?? defaultConfig(), { allowDemo: true });
|
|
1627
|
+
if (made) cfg = made;
|
|
1628
|
+
else {
|
|
1629
|
+
args.demo = true;
|
|
1630
|
+
cfg = { ...defaultConfig(), activeProfile: "demo", profiles: { demo: DEMO_PROFILE } };
|
|
1631
|
+
}
|
|
1583
1632
|
}
|
|
1584
1633
|
let profileName = args.profile ?? cfg.activeProfile;
|
|
1585
1634
|
if (!cfg.profiles[profileName]) {
|
|
1586
1635
|
console.error(`Unknown profile "${profileName}". Available: ${Object.keys(cfg.profiles).join(", ")}`);
|
|
1587
1636
|
process.exit(1);
|
|
1588
1637
|
}
|
|
1638
|
+
const needsKey = (p) => !args.demo && !resolveApiKey(p) && !(p.type === "openai" && isLocalUrl(p.baseUrl));
|
|
1639
|
+
if (needsKey(cfg.profiles[profileName]) && process.stdin.isTTY) {
|
|
1640
|
+
const p = cfg.profiles[profileName];
|
|
1641
|
+
console.log(theme.muted(`No API key for "${profileName}" yet.`));
|
|
1642
|
+
const key = await askKey(ask, profileName);
|
|
1643
|
+
if (key) {
|
|
1644
|
+
p.apiKey = key;
|
|
1645
|
+
saveConfig(cfg);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1589
1648
|
const ui = new Ui({ theme, pet: frogPet, showPet: cfg.pet && !args.noPet, animations: cfg.animations, ask });
|
|
1590
1649
|
const permissions = new Permissions(args.yolo);
|
|
1591
1650
|
const persist = !args.demo;
|
|
@@ -1716,7 +1775,7 @@ async function main() {
|
|
|
1716
1775
|
},
|
|
1717
1776
|
model: async (arg) => {
|
|
1718
1777
|
if (arg === "add") {
|
|
1719
|
-
cfg = await setupProfile(ask, cfg);
|
|
1778
|
+
cfg = await setupProfile(ask, cfg) ?? cfg;
|
|
1720
1779
|
arg = cfg.activeProfile;
|
|
1721
1780
|
}
|
|
1722
1781
|
if (!arg) {
|