croo-cli 0.1.0 → 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 -33
  2. package/dist/cli.js +155 -31
  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,26 +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
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
37
75
  ```
38
76
 
39
- Slash commands: `/help`, `/model` (list, `/model <name>` to switch, `/model add`), `/clear`, `/config`, `/exit`.
40
- `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.
41
88
 
42
- ## Configuration
89
+ ## Configure providers
43
90
 
44
- `~/.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`.
45
92
 
46
93
  ```jsonc
47
94
  {
@@ -49,9 +96,9 @@ Slash commands: `/help`, `/model` (list, `/model <name>` to switch, `/model add`
49
96
  "pet": true,
50
97
  "animations": true,
51
98
  "profiles": {
52
- "openai": { "type": "openai", "model": "gpt-4o" },
99
+ "openai": { "type": "openai", "model": "gpt-4o" },
53
100
  "anthropic": { "type": "anthropic", "model": "claude-sonnet-4-5" },
54
- "gemini": { "type": "gemini", "model": "gemini-2.5-pro" },
101
+ "gemini": { "type": "gemini", "model": "gemini-2.5-pro" },
55
102
  "openrouter": {
56
103
  "type": "openai",
57
104
  "baseUrl": "https://openrouter.ai/api/v1",
@@ -66,6 +113,8 @@ Slash commands: `/help`, `/model` (list, `/model <name>` to switch, `/model add`
66
113
  }
67
114
  ```
68
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
+
69
118
  Optional per-profile `inputPerMTok` / `outputPerMTok` (USD per million tokens) adds a cost estimate to the status line.
70
119
 
71
120
  ## Tools and permissions
@@ -73,36 +122,48 @@ Optional per-profile `inputPerMTok` / `outputPerMTok` (USD per million tokens) a
73
122
  | Tool | What it does | Asks first? |
74
123
  | --- | --- | --- |
75
124
  | `read_file` | Read a file with line numbers | no |
76
- | `list_files` | List/glob files (respects `.gitignore`) | no |
125
+ | `list_files` | List or glob files (respects `.gitignore`) | no |
77
126
  | `grep` | Regex search (respects `.gitignore`) | no |
78
127
  | `write_file` | Create or overwrite a file | yes, shows a diff |
79
128
  | `edit_file` | Exact string replace | yes, shows a diff |
80
- | `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.
81
134
 
82
- 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`.
83
136
 
84
- 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.
85
140
 
86
141
  ## Development
87
142
 
88
143
  ```bash
89
- 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
90
149
  ```
91
150
 
92
- See [CONTRIBUTING.md](CONTRIBUTING.md).
151
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and pull requests are welcome.
93
152
 
94
153
  ## Roadmap
95
154
 
96
- - Custom pets: the `Pet` interface in `src/ui/pet.ts` is designed so you can drop in your own frames
97
- - Idle animation at the prompt
98
- - Context window management and compaction
99
- - MCP support
100
- - 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
101
160
 
102
161
  ## License
103
162
 
104
- MIT, see [LICENSE](LICENSE).
163
+ [MIT](LICENSE), Copyright (c) 2026 Xander Taylor. Free to use, change and share.
164
+
165
+ <div align="center">
105
166
 
106
- ---
167
+ <sub>An Entity of The Tizzle Collective</sub>
107
168
 
108
- 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.0",
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: {
@@ -59,7 +59,7 @@ var package_default = {
59
59
 
60
60
  // src/args.ts
61
61
  function parseArgs(argv) {
62
- const a = { prompt: "", yolo: false, noPet: false, continue: false, help: false, version: false };
62
+ const a = { prompt: "", yolo: false, noPet: false, demo: false, continue: false, help: false, version: false };
63
63
  const rest = [];
64
64
  for (let i = 0; i < argv.length; i++) {
65
65
  const x = argv[i];
@@ -67,6 +67,7 @@ function parseArgs(argv) {
67
67
  else if (x.startsWith("--profile=")) a.profile = x.slice("--profile=".length);
68
68
  else if (x === "--yolo") a.yolo = true;
69
69
  else if (x === "--no-pet") a.noPet = true;
70
+ else if (x === "--demo") a.demo = true;
70
71
  else if (x === "--continue" || x === "-c") a.continue = true;
71
72
  else if (x === "--help" || x === "-h") a.help = true;
72
73
  else if (x === "--version" || x === "-v") a.version = true;
@@ -85,6 +86,7 @@ Options:
85
86
  -c, --continue resume the last session in this directory
86
87
  --yolo do not ask before writing files or running commands
87
88
  --no-pet hide the frog
89
+ --demo try the interface with a scripted fake model (no API key)
88
90
  -v, --version print version
89
91
  -h, --help show this help
90
92
 
@@ -621,6 +623,59 @@ function buildSystemPrompt(cwd2, now = /* @__PURE__ */ new Date()) {
621
623
  return parts.join("\n\n");
622
624
  }
623
625
 
626
+ // src/providers/demo.ts
627
+ var DEMO_PROFILE = { type: "openai", model: "demo (no API)" };
628
+ var sleep = (ms, signal) => new Promise((resolve) => {
629
+ const t = setTimeout(resolve, ms);
630
+ signal?.addEventListener("abort", () => (clearTimeout(t), resolve()), { once: true });
631
+ });
632
+ var INTRO = `## Hello from the demo
633
+ I am **not** a real model, so no API key is needed. Here is what Croo's output looks like:
634
+
635
+ - lists with bullets
636
+ - inline \`code\` and **bold**
637
+
638
+ \`\`\`ts
639
+ const frog = "ribbit"
640
+ console.log(frog)
641
+ \`\`\`
642
+
643
+ Let me look around, then propose a file so you can try the permission prompt.
644
+ `;
645
+ var OUTRO = `All done. That was the demo: tool cards, a diff prompt, streaming and the spinner.
646
+ Run \`croo\` without \`--demo\` to set up a real provider.
647
+ `;
648
+ async function stream(text, onText, signal) {
649
+ for (let i = 0; i < text.length && !signal?.aborted; i += 6) {
650
+ onText(text.slice(i, i + 6));
651
+ await sleep(12, signal);
652
+ }
653
+ }
654
+ var lastUser = (req) => req.messages.map((m) => m.role).lastIndexOf("user");
655
+ function demoProvider() {
656
+ return {
657
+ async chat(req, onText) {
658
+ const usage = { input: 120, output: 80 };
659
+ await sleep(900, req.signal);
660
+ const last = req.messages[req.messages.length - 1];
661
+ if (last?.role === "user") {
662
+ await stream(INTRO, onText, req.signal);
663
+ return { text: INTRO, usage, toolCalls: [{ id: "demo1", name: "list_files", args: {} }] };
664
+ }
665
+ const wrote = req.messages.some((m, i) => m.role === "tool" && m.name === "write_file" && i > lastUser(req));
666
+ if (!wrote) {
667
+ return {
668
+ text: "",
669
+ usage,
670
+ toolCalls: [{ id: "demo2", name: "write_file", args: { path: "croo-demo.txt", content: "ribbit\nCroo was here.\n" } }]
671
+ };
672
+ }
673
+ await stream(OUTRO, onText, req.signal);
674
+ return { text: OUTRO, usage, toolCalls: [] };
675
+ }
676
+ };
677
+ }
678
+
624
679
  // src/providers/sse.ts
625
680
  async function* sseData(res) {
626
681
  if (!res.body) throw new Error("Response has no body.");
@@ -882,6 +937,20 @@ function createProvider(profile, env = process.env) {
882
937
  return openaiProvider(profile, key);
883
938
  }
884
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
+
885
954
  // src/session.ts
886
955
  import fs5 from "fs";
887
956
  import path5 from "path";
@@ -1266,7 +1335,7 @@ function collapseContext(lines, context = 2) {
1266
1335
  }
1267
1336
 
1268
1337
  // src/ui/render.ts
1269
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1338
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1270
1339
  var fmtTokens = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(n >= 1e4 ? 0 : 1)}k` : String(n);
1271
1340
  var Ui = class {
1272
1341
  constructor(o) {
@@ -1319,9 +1388,9 @@ var Ui = class {
1319
1388
  };
1320
1389
  hideCursor();
1321
1390
  try {
1322
- await sleep(550);
1391
+ await sleep2(550);
1323
1392
  redraw(frames[1]);
1324
- await sleep(140);
1393
+ await sleep2(140);
1325
1394
  redraw(frames[0]);
1326
1395
  } finally {
1327
1396
  showCursor();
@@ -1475,27 +1544,63 @@ var ask = (question, opts) => {
1475
1544
  muted = !!opts?.secret;
1476
1545
  });
1477
1546
  };
1478
- var DEFAULT_MODELS = { openai: "gpt-4o", anthropic: "claude-sonnet-4-5", gemini: "gemini-2.5-pro" };
1479
- async function setupProfile(a, cfg) {
1480
- console.log(theme.bold("\nSet up a provider") + theme.muted(" (stored in " + configPath() + ", mode 600)\n"));
1481
- console.log(" 1) OpenAI-compatible (OpenAI, OpenRouter, Groq, Ollama, LM Studio, ...)\n 2) Anthropic\n 3) Google Gemini\n");
1482
- const choice = (await a("Provider [1]: ")).trim() || "1";
1483
- const type = choice === "2" ? "anthropic" : choice === "3" ? "gemini" : "openai";
1484
- const profile = { type, model: "" };
1485
- if (type === "openai") {
1486
- const url = (await a(`Base URL [${DEFAULT_BASE_URLS.openai}] (Ollama: http://localhost:11434/v1): `)).trim();
1487
- if (url) profile.baseUrl = url;
1488
- }
1489
- const key = (await a("API key (leave blank to use an env var, or for local models): ", { secret: true })).trim();
1490
- if (key) profile.apiKey = key;
1491
- profile.model = (await a(`Model [${DEFAULT_MODELS[type]}]: `)).trim() || DEFAULT_MODELS[type];
1492
- 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
+ }
1493
1598
  let name = base;
1494
1599
  for (let i = 2; cfg.profiles[name]; i++) name = `${base}${i}`;
1495
1600
  const next = { ...cfg, profiles: { ...cfg.profiles, [name]: profile }, activeProfile: name };
1496
1601
  saveConfig(next);
1497
1602
  console.log(theme.accent(`
1498
- Saved profile "${name}".
1603
+ Saved. Using ${profile.model}. Change it any time with /model.
1499
1604
  `));
1500
1605
  return next;
1501
1606
  }
@@ -1509,26 +1614,44 @@ async function main() {
1509
1614
  }
1510
1615
  if (args.help) return console.log(HELP);
1511
1616
  if (args.version) return console.log(VERSION);
1512
- let cfg = loadConfig();
1513
- if (!cfg || !Object.keys(cfg.profiles).length) {
1617
+ let cfg = args.demo ? null : loadConfig();
1618
+ if (args.demo) {
1619
+ cfg = { ...defaultConfig(), activeProfile: "demo", profiles: { demo: DEMO_PROFILE } };
1620
+ } else if (!cfg || !Object.keys(cfg.profiles).length) {
1514
1621
  if (!process.stdin.isTTY) {
1515
1622
  console.error(`No config found. Run \`croo\` in a terminal to set one up, or create ${configPath()}.`);
1516
1623
  process.exit(1);
1517
1624
  }
1518
- console.log(theme.accent(theme.bold("Welcome to Croo.")) + " No config yet, so let us set one up.");
1519
- 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
+ }
1520
1632
  }
1521
1633
  let profileName = args.profile ?? cfg.activeProfile;
1522
1634
  if (!cfg.profiles[profileName]) {
1523
1635
  console.error(`Unknown profile "${profileName}". Available: ${Object.keys(cfg.profiles).join(", ")}`);
1524
1636
  process.exit(1);
1525
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
+ }
1526
1648
  const ui = new Ui({ theme, pet: frogPet, showPet: cfg.pet && !args.noPet, animations: cfg.animations, ask });
1527
1649
  const permissions = new Permissions(args.yolo);
1650
+ const persist = !args.demo;
1528
1651
  const interactive = !args.prompt;
1529
1652
  let provider;
1530
1653
  const useProfile = (name) => {
1531
- provider = createProvider(cfg.profiles[name]);
1654
+ provider = args.demo ? demoProvider() : createProvider(cfg.profiles[name]);
1532
1655
  profileName = name;
1533
1656
  };
1534
1657
  try {
@@ -1537,7 +1660,7 @@ async function main() {
1537
1660
  ui.petSays("oops", e.message);
1538
1661
  process.exit(1);
1539
1662
  }
1540
- let session = args.continue && loadLastSession(cwd) || newSession(cwd, profileName);
1663
+ let session = args.continue && !args.demo && loadLastSession(cwd) || newSession(cwd, profileName);
1541
1664
  const messages = session.messages;
1542
1665
  if (args.continue && messages.length) ui.info(`Resumed session with ${messages.length} messages.`);
1543
1666
  const system = buildSystemPrompt(cwd);
@@ -1581,7 +1704,7 @@ async function main() {
1581
1704
  ui.idle();
1582
1705
  current = void 0;
1583
1706
  session.profile = profileName;
1584
- saveSession(session);
1707
+ if (persist) saveSession(session);
1585
1708
  }
1586
1709
  }
1587
1710
  function interrupt() {
@@ -1613,6 +1736,7 @@ async function main() {
1613
1736
  const profile = () => cfg.profiles[profileName];
1614
1737
  const showBanner = () => ui.banner({ version: VERSION, profile: profileName, model: profile().model, cwd });
1615
1738
  await showBanner();
1739
+ if (args.demo) ui.info("Demo mode: a scripted fake model, no API calls. Type anything to start.");
1616
1740
  const reader = getRl();
1617
1741
  reader.setPrompt(theme.accent("\u203A "));
1618
1742
  reader.on("SIGINT", interrupt);
@@ -1651,7 +1775,7 @@ async function main() {
1651
1775
  },
1652
1776
  model: async (arg) => {
1653
1777
  if (arg === "add") {
1654
- cfg = await setupProfile(ask, cfg);
1778
+ cfg = await setupProfile(ask, cfg) ?? cfg;
1655
1779
  arg = cfg.activeProfile;
1656
1780
  }
1657
1781
  if (!arg) {
@@ -1662,7 +1786,7 @@ async function main() {
1662
1786
  try {
1663
1787
  useProfile(arg);
1664
1788
  cfg.activeProfile = arg;
1665
- saveConfig(cfg);
1789
+ if (persist) saveConfig(cfg);
1666
1790
  ui.info(` now using ${arg} (${profile().model})`);
1667
1791
  } catch (e) {
1668
1792
  ui.error(e.message);
@@ -1682,7 +1806,7 @@ async function main() {
1682
1806
  }
1683
1807
  next();
1684
1808
  }
1685
- saveSession(session);
1809
+ if (persist) saveSession(session);
1686
1810
  process.exit(0);
1687
1811
  }
1688
1812
  main().catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "croo-cli",
3
- "version": "0.1.0",
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": {