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.
Files changed (3) hide show
  1. package/README.md +94 -34
  2. package/dist/cli.js +85 -26
  3. 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 quick, minimal, open-source terminal AI coding agent. Bring your own API key (or a local model) and let a small frog help you code.
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
+ [![npm](https://img.shields.io/npm/v/croo-cli?color=5ed67e&label=npm)](https://www.npmjs.com/package/croo-cli)
16
+ [![CI](https://github.com/xandertaylorx/croo/actions/workflows/ci.yml/badge.svg)](https://github.com/xandertaylorx/croo/actions/workflows/ci.yml)
17
+ [![License: MIT](https://img.shields.io/badge/license-MIT-5ed67e)](LICENSE)
18
+ [![Node](https://img.shields.io/badge/node-%E2%89%A520-5ed67e)](https://nodejs.org)
19
+ [![Dependencies](https://img.shields.io/badge/runtime%20deps-0-5ed67e)](package.json)
20
+
21
+ </div>
4
22
 
5
23
  ```
6
24
  ▄▄▄ ▄▄▄ “ribbit. ready when you are.”
7
- ██▄█▄▄▄█▄██ Croo v0.1.0
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
- - **Fully local.** No accounts, no login, no telemetry, no backend. Config lives in `~/.config/croo/config.json`.
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
- Croo works with API keys and any OpenAI-compatible endpoint, including local models. It does not use anyone's consumer-subscription logins.
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
- Requires Node 20+.
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 # first run walks you through setup
32
- croo "add a --verbose flag to the CLI" # one-shot
33
- croo --continue # resume the last session in this directory
34
- croo --profile local # use a named profile
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
- Slash commands: `/help`, `/model` (list, `/model <name>` to switch, `/model add`), `/clear`, `/config`, `/exit`.
41
- `Ctrl+C` cancels the current generation; a second `Ctrl+C` exits.
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
- ## Configuration
89
+ ## Configure providers
44
90
 
45
- `~/.config/croo/config.json` (created with mode 600). API keys may live here or in env vars: `CROO_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`. Priority is the profile's `apiKey`, then `CROO_API_KEY`, then the provider's own variable.
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": { "type": "openai", "model": "gpt-4o" },
99
+ "openai": { "type": "openai", "model": "gpt-4o" },
54
100
  "anthropic": { "type": "anthropic", "model": "claude-sonnet-4-5" },
55
- "gemini": { "type": "gemini", "model": "gemini-2.5-pro" },
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/glob files (respects `.gitignore`) | no |
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 output) | yes, shows the command |
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
- Prompts accept `y`, `n` or `a` (always, for that tool, this session). File tools cannot leave the current directory. Put project guidance in `CROO.md` or `AGENTS.md` and Croo will include it in its system prompt.
135
+ **Sessions** are saved as plain JSON in `~/.local/share/croo/sessions`.
84
136
 
85
- Sessions are saved as JSON under `~/.local/share/croo/sessions`.
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
- npm install && npm test && npm run build
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 designed so you can drop in your own frames
98
- - Idle animation at the prompt
99
- - Context window management and compaction
100
- - MCP support
101
- - More tools (web fetch, multi-edit)
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, see [LICENSE](LICENSE).
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
- An Entity of The Tizzle Collective
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.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
- var DEFAULT_MODELS = { openai: "gpt-4o", anthropic: "claude-sonnet-4-5", gemini: "gemini-2.5-pro" };
1534
- async function setupProfile(a, cfg) {
1535
- console.log(theme.bold("\nSet up a provider") + theme.muted(" (stored in " + configPath() + ", mode 600)\n"));
1536
- console.log(" 1) OpenAI-compatible (OpenAI, OpenRouter, Groq, Ollama, LM Studio, ...)\n 2) Anthropic\n 3) Google Gemini\n");
1537
- const choice = (await a("Provider [1]: ")).trim() || "1";
1538
- const type = choice === "2" ? "anthropic" : choice === "3" ? "gemini" : "openai";
1539
- const profile = { type, model: "" };
1540
- if (type === "openai") {
1541
- const url = (await a(`Base URL [${DEFAULT_BASE_URLS.openai}] (Ollama: http://localhost:11434/v1): `)).trim();
1542
- if (url) profile.baseUrl = url;
1543
- }
1544
- const key = (await a("API key (leave blank to use an env var, or for local models): ", { secret: true })).trim();
1545
- if (key) profile.apiKey = key;
1546
- profile.model = (await a(`Model [${DEFAULT_MODELS[type]}]: `)).trim() || DEFAULT_MODELS[type];
1547
- let base = type === "openai" && profile.baseUrl ? new URL(profile.baseUrl).hostname.replace(/^api\./, "").split(".")[0] : type;
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 profile "${name}".
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
- cfg = await setupProfile(ask, cfg ?? defaultConfig());
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "croo-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Croo: a quick, minimal, open-source terminal AI coding agent. Bring your own API key.",
5
5
  "type": "module",
6
6
  "bin": {