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.
- package/README.md +94 -33
- package/dist/cli.js +155 -31
- 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,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
|
-
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
##
|
|
89
|
+
## Configure providers
|
|
43
90
|
|
|
44
|
-
`~/.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`.
|
|
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":
|
|
99
|
+
"openai": { "type": "openai", "model": "gpt-4o" },
|
|
53
100
|
"anthropic": { "type": "anthropic", "model": "claude-sonnet-4-5" },
|
|
54
|
-
"gemini":
|
|
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
|
|
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
|
|
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
|
-
|
|
135
|
+
**Sessions** are saved as plain JSON in `~/.local/share/croo/sessions`.
|
|
83
136
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
|
97
|
-
- Idle animation at the prompt
|
|
98
|
-
- Context
|
|
99
|
-
- MCP support
|
|
100
|
-
- 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
|
|
101
160
|
|
|
102
161
|
## License
|
|
103
162
|
|
|
104
|
-
MIT,
|
|
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
|
-
|
|
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: {
|
|
@@ -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
|
|
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
|
|
1391
|
+
await sleep2(550);
|
|
1323
1392
|
redraw(frames[1]);
|
|
1324
|
-
await
|
|
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
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
}
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
|
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 (
|
|
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."))
|
|
1519
|
-
|
|
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) => {
|