nou-cli 0.5.1 → 0.5.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 +40 -6
- package/dist/index.js +9 -99
- package/dist/src/agent/index.js +169 -33
- package/dist/src/cli.js +115 -0
- package/dist/src/commands/index.js +233 -25
- package/dist/src/config/index.js +14 -0
- package/dist/src/context/index.js +33 -10
- package/dist/src/games/index.js +217 -0
- package/dist/src/mcp/import.js +106 -0
- package/dist/src/mcp/index.js +184 -0
- package/dist/src/providers/anthropic.js +33 -2
- package/dist/src/providers/index.js +13 -0
- package/dist/src/providers/openai.js +6 -2
- package/dist/src/syntax/index.js +10 -20
- package/dist/src/syntax/themes.js +57 -0
- package/dist/src/tools/index.js +86 -2
- package/dist/src/ui/App.js +675 -112
- package/dist/src/ui/Chat.js +84 -21
- package/dist/src/ui/ChatInput.js +4 -2
- package/dist/src/ui/CodeView.js +110 -19
- package/dist/src/ui/Editor.js +13 -6
- package/dist/src/ui/FileTree.js +1 -1
- package/dist/src/ui/GamePanel.js +92 -0
- package/dist/src/ui/Header.js +1 -1
- package/dist/src/ui/QuickOpen.js +51 -12
- package/dist/src/ui/Settings.js +58 -13
- package/dist/src/ui/Tabs.js +55 -0
- package/dist/src/ui/clipboard.js +9 -7
- package/dist/src/ui/commands.js +33 -0
- package/dist/src/ui/definition.js +135 -0
- package/dist/src/ui/fast-width.js +62 -0
- package/dist/src/ui/mouse.js +4 -1
- package/dist/src/ui/text.js +116 -12
- package/dist/src/ui/width-shim.js +7 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -49,12 +49,15 @@ Needs **Node 22.12+** and an interactive terminal (Windows Terminal, PowerShell,
|
|
|
49
49
|
| 🗂️ **File explorer** | Browse your project with file icons and git status (`M` modified, `?` new). Press <kbd>c</kbd> to see only changed files. |
|
|
50
50
|
| 🔀 **Live diffs** | Every edit Nou proposes shows up side by side. <kbd>y</kbd> accepts, <kbd>n</kbd> rejects. Nothing touches disk without you. |
|
|
51
51
|
| ✍️ **Built-in editor** | Press <kbd>e</kbd> to edit the open file yourself, with undo/redo and **auto-save**. |
|
|
52
|
+
| 📍 **Shows you where things are** | Ask "where is the LiveKit integration?" and Nou finds it and opens it in the middle panel, jumped to the exact lines and highlighted. Any file a reply mentions (`src/a.ts:12`) is a link: click it. `/open name[:line]` opens a file yourself. |
|
|
53
|
+
| 🎮 **Games for a break** | Add Pong, Snake and Breakout in settings → Games, then `/games` opens a floating panel you can drag anywhere by its title. A game runs only while the panel has the keyboard: Esc goes back to your code and it waits. |
|
|
54
|
+
| 🧠 **A capable engine** | Reads and searches in parallel, numbered reads for exact line references, edits that survive small whitespace mismatches, several edits to a file as one change, a plan for longer tasks, full command output (stderr too) with exit codes and timeouts, retries when a provider is overloaded, and conversation caching with Claude. It follows your repo's `AGENTS.md`, `CLAUDE.md` and `NOU.md` (and `~/.nou/NOU.md`). |
|
|
52
55
|
| 🔎 **Finds its way around** | The agent searches your code (`grep`, `glob`) without asking, reads only what it needs, runs your tests to check its work, and summarizes long conversations to stay within the model's context (`/compact` does it on demand). |
|
|
53
56
|
| ↶ **Undo anything** | Every change, Nou's or yours, is recorded. <kbd>u</kbd> undoes Nou's last edit, `/rewind` undoes its whole last turn, and your own later edits are kept. |
|
|
54
57
|
| 🧾 **Session diffs** | `/diff` shows everything that changed this session, file by file (<kbd>[</kbd> <kbd>]</kbd> to step through). `/sessions` lists earlier sessions and `/diff 2` shows one. |
|
|
55
58
|
| 🩺 **Problems, live** | Syntax errors instantly, plus ESLint, TypeScript, Ruff and language servers when your project has them. Shown in the gutter, inline, and in a <kbd>p</kbd> Problems panel. |
|
|
56
59
|
| 🤖 **Self-correcting** | Nou checks its own edits: you see "⚠ +1 problem" *before* approving, and any problems it introduced are fed back so it fixes them. |
|
|
57
|
-
| 🎨 **Real syntax highlighting** | tree-sitter parsing for 14 languages (TS, JS, Python, Go, Rust, Java, C, JSON, YAML…). |
|
|
60
|
+
| 🎨 **Real syntax highlighting** | tree-sitter parsing for 14 languages (TS, JS, Python, Go, Rust, Java, C, JSON, YAML…), in your choice of theme: One Dark (default), Tokyo Night, GitHub Dark, Dracula, High Contrast or GitHub Light (settings → Preferences → Code colors, with a preview). |
|
|
58
61
|
| 🧭 **Code intelligence** | <kbd>Ctrl</kbd>+<kbd>K</kbd> hover info and <kbd>Ctrl</kbd>+<kbd>G</kbd> go-to-definition via language servers (typescript-language-server, pyright, rust-analyzer, gopls, clangd). |
|
|
59
62
|
| 👀 **Knows what you're looking at** | The open file, visible lines, your cursor and unsaved edits ride along with each message. "Fix this function" just works. |
|
|
60
63
|
| 🌿 **Git aware** | Sees your branch, uncommitted changes and recent commits, and can diff on its own. Press <kbd>v</kbd> to diff any file against `HEAD`. |
|
|
@@ -78,21 +81,28 @@ Every change is reviewed like a code review:
|
|
|
78
81
|
| --- | --- |
|
|
79
82
|
| 🖱️ Mouse | wheel scrolls the pane under the pointer (<kbd>Shift</kbd>+wheel or a tilt wheel: sideways) · click or drag a scrollbar to jump · click a file to open it · **double-click in the code to edit right there** (in the editor: select a word), drag to select · drag a border between panes to resize them (press <kbd>m</kbd> to turn the mouse off and select text in your terminal) |
|
|
80
83
|
| <kbd>PgUp</kbd>/<kbd>PgDn</kbd>, <kbd>Shift</kbd>+<kbd>↑</kbd>/<kbd>↓</kbd> | scroll the chat; scrolled up, new replies wait below instead of yanking the view (<kbd>Ctrl</kbd>+<kbd>End</kbd> jumps back) |
|
|
81
|
-
| <kbd
|
|
84
|
+
| <kbd>?</kbd> | **every action, searchable**, each with its shortcut (also <kbd>Ctrl</kbd>+<kbd>P</kbd> then `>`) |
|
|
85
|
+
| <kbd>/</kbd> | start a command from any pane: a menu lists every command with what it does (<kbd>↑</kbd>/<kbd>↓</kbd> choose, <kbd>Tab</kbd> complete, <kbd>Enter</kbd> run) |
|
|
86
|
+
| 🖱️ Status bar | click the mode to switch it, `follow` to toggle it, the ✖/▲ counts for the Problems panel, or any shortcut hint to do it · click the model in the header to change it |
|
|
82
87
|
| <kbd>Tab</kbd> | switch between files, code and chat |
|
|
83
88
|
| <kbd>Shift</kbd>+<kbd>Tab</kbd> | ask → auto-edit (edits apply without asking) → auto (commands too); everything stays undoable |
|
|
84
|
-
| <kbd>Ctrl</kbd>+<kbd>P</kbd> | fuzzy-find a file |
|
|
89
|
+
| <kbd>Ctrl</kbd>+<kbd>P</kbd> | fuzzy-find a file; the files you've opened are listed first |
|
|
90
|
+
| <kbd>b</kbd> | back to the previous file (press again to flip back) |
|
|
91
|
+
| <kbd>Ctrl</kbd>+<kbd>F</kbd> | find in the open file: matches are highlighted and counted; <kbd>Enter</kbd>/<kbd>↓</kbd> next, <kbd>↑</kbd> previous, <kbd>Esc</kbd> close |
|
|
92
|
+
| 🗂️ Tabs | every file you open gets a tab over the code: click one to switch, its × to close · <kbd>[</kbd>/<kbd>]</kbd> previous/next · <kbd>x</kbd> close |
|
|
93
|
+
| <kbd>Ctrl</kbd>+click | on a name in the code (or <kbd>Option</kbd>+click, or right-click): go to its definition; an imported name goes to its import, and the import's path opens that file |
|
|
85
94
|
| <kbd>e</kbd> or a double-click | edit the open file (see the editor keys below) |
|
|
86
95
|
| <kbd>p</kbd> | Problems panel: every error and warning, <kbd>Enter</kbd> jumps to it |
|
|
87
96
|
| <kbd>u</kbd> | undo Nou's last edit |
|
|
88
97
|
| <kbd>v</kbd> | diff the open file vs git `HEAD`, Nou's last edit, or this session · <kbd>n</kbd>/<kbd>N</kbd> jump between changes · <kbd>[</kbd>/<kbd>]</kbd> previous/next file in a session diff |
|
|
89
98
|
| <kbd>c</kbd> | in the file tree: show only changed files |
|
|
90
|
-
| <kbd>f</kbd> | follow mode (off by default):
|
|
99
|
+
| <kbd>f</kbd> | follow mode (off by default): watch along as Nou works. Each file it reads (the exact lines, highlighted) is shown a step at a time, a little behind it, and its edits as they happen. The status bar shows how many steps behind it is; set the speed in settings → Preferences (slow · normal · fast · instant) |
|
|
91
100
|
| <kbd><</kbd> / <kbd>></kbd> / <kbd>=</kbd> | make the focused pane narrower / wider / reset the layout |
|
|
92
101
|
| <kbd>m</kbd> | mouse on/off, so you can select and copy text in the terminal |
|
|
93
102
|
| <kbd>s</kbd> | settings: model, preferences, plugins |
|
|
94
103
|
| <kbd>y</kbd> / <kbd>n</kbd> | accept / reject a proposed edit or command, from any pane; the rest of Nou keeps working while it waits (<kbd>o</kbd> shows the edit) |
|
|
95
|
-
| <kbd>
|
|
104
|
+
| <kbd>Enter</kbd> while Nou works | queue the message: it goes when Nou finishes (<kbd>Alt</kbd>+<kbd>Enter</kbd>: stop Nou and send it now) |
|
|
105
|
+
| <kbd>Esc</kbd> | stop Nou (anything queued goes back in the input) · close a diff · back to chat |
|
|
96
106
|
| <kbd>Ctrl</kbd>+<kbd>C</kbd> | stop Nou while it works · press twice to quit |
|
|
97
107
|
|
|
98
108
|
In the editor:
|
|
@@ -165,6 +175,20 @@ nou --version
|
|
|
165
175
|
|
|
166
176
|
Set `NOU_NO_UPDATE_CHECK=1` to turn the check off.
|
|
167
177
|
|
|
178
|
+
## 🔌 MCP servers and browser control
|
|
179
|
+
|
|
180
|
+
Nou's engine can use tools from [MCP](https://modelcontextprotocol.io) servers: a browser, GitHub, your database...
|
|
181
|
+
|
|
182
|
+
- **Already use some?** Nou finds the MCP servers you've set up in Claude Code, Claude Desktop, Cursor, Windsurf, VS Code, Gemini CLI and Codex, and offers them on start: `/mcp import` brings them over, with their settings. (Your Claude Code and Codex API keys are picked up too; see *Use Claude Code / Codex keys* in settings.)
|
|
183
|
+
- **Browser control:** `/mcp add browser` adds [Playwright's MCP server](https://github.com/microsoft/playwright-mcp), so you can ask Nou to open a page, click through it, fill a form or check what your app shows.
|
|
184
|
+
- **Your own:** `/mcp add <name> <command and arguments>` or `/mcp add <name> <url>`; `/mcp` lists them and their status, `/mcp remove <name>` removes one. They live in `~/.nou/mcp.json` (same format as Claude Code and Cursor); a project's `.mcp.json` is used too, in folders you trust.
|
|
185
|
+
|
|
186
|
+
Tools that say they only read run on their own; any other MCP tool asks you first, like Nou's edits and commands. A server's own output never reaches Nou's screen.
|
|
187
|
+
|
|
188
|
+
## 🔒 Trusting a folder
|
|
189
|
+
|
|
190
|
+
The first time you open a folder, Nou asks whether you trust its files: it reads them, and can run the folder's MCP servers and plugins. Say no and Nou doesn't open it. Trusted folders are listed in `~/.nou/config.json`.
|
|
191
|
+
|
|
168
192
|
## 🧩 Plugins *(early)*
|
|
169
193
|
|
|
170
194
|
**Starter plugins** ship with Nou and are optional: open <kbd>s</kbd> settings → **Plugins** and press Enter on one, or type `/plugins add <name>`. Adding one copies it into `~/.nou/plugins/`, where it's yours to edit.
|
|
@@ -207,7 +231,17 @@ npm run screenshot # regenerate docs/screenshot.svg
|
|
|
207
231
|
npm run eval # how well the agent does real tasks with a real model (see evals/)
|
|
208
232
|
```
|
|
209
233
|
|
|
210
|
-
Releasing
|
|
234
|
+
### Releasing
|
|
235
|
+
|
|
236
|
+
**Always bump the version first.** npm never accepts the same version twice, so publishing without a bump fails with *"nou-cli@X.Y.Z is already published. Bump "version" in package.json first."* Every release goes:
|
|
237
|
+
|
|
238
|
+
1. **Bump the version** in a PR: `npm version patch --no-git-tag-version` (or `minor` / `major`), which updates `package.json` and `package-lock.json` together. Patch (0.5.1 → 0.5.2) for fixes and small features, minor (0.5 → 0.6) for bigger ones.
|
|
239
|
+
2. **Merge it** to `master` once CI is green.
|
|
240
|
+
3. **Run Actions → Publish to npm** (Run workflow on `master`), then **approve** the `production` environment when GitHub asks. It tests, publishes with provenance, and creates the `vX.Y.Z` git tag and GitHub Release. (Publishing a GitHub release tagged `vX.Y.Z` does the same, taking the version from the tag.)
|
|
241
|
+
|
|
242
|
+
The npm token lives in the `production` environment as `NPM_TOKEN`. Check what's live with `npm view nou-cli version`.
|
|
243
|
+
|
|
244
|
+
The workflows run on the actions' Node 24 versions (`actions/checkout@v5`, `actions/setup-node@v5`, `codeql-action@v4`), so GitHub's Node 20 deprecation warning no longer applies; Dependabot keeps them current. Jobs run on `ubuntu-latest`, which GitHub moves to Ubuntu 26 from October 19, 2026: nothing to change for that.
|
|
211
245
|
|
|
212
246
|
Upkeep runs on its own:
|
|
213
247
|
- **CI** on every PR: type-check, lint, build, unit and end-to-end tests on Linux, macOS and Windows (Node 22.12 and 24 too), plus installing the packed tarball to check the package works.
|
package/dist/index.js
CHANGED
|
@@ -1,103 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Must stay the first import: loads .env before any provider reads its keys and URLs.
|
|
3
3
|
import "./src/env.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { describe, initialModel, resolveModel, saveModel } from "./src/providers/index.js";
|
|
11
|
-
import { loadSettings } from "./src/config/index.js";
|
|
12
|
-
import { loadCredentials } from "./src/config/credentials.js";
|
|
13
|
-
import { borrowToolConfig, ensureLocalModel, pickStartingModel } from "./src/setup/index.js";
|
|
14
|
-
import { checkForUpdate, currentVersion, installUpdate, relaunch } from "./src/update/index.js";
|
|
15
|
-
import { lsp } from "./src/lsp/index.js";
|
|
16
|
-
import { App } from "./src/ui/App.js";
|
|
17
|
-
import { installFastOutput } from "./src/ui/fast-output.js";
|
|
18
|
-
import { RENDER_OPTIONS } from "./src/ui/render-options.js";
|
|
19
|
-
const HELP = `nou ${currentVersion()}: an AI coding assistant for your terminal
|
|
20
|
-
|
|
21
|
-
Usage:
|
|
22
|
-
nou open Nou in the current folder
|
|
23
|
-
nou update update to the latest version
|
|
24
|
-
nou --version print the version
|
|
25
|
-
nou --help show this help
|
|
26
|
-
|
|
27
|
-
Settings live in ~/.nou/config.json (or press s inside Nou); keys you enter there, or with /connect, in ~/.nou/credentials.json. Set NOU_NO_UPDATE_CHECK=1 to skip update checks.`;
|
|
28
|
-
async function main() {
|
|
29
|
-
const args = process.argv.slice(2);
|
|
30
|
-
if (args.includes("--version") || args.includes("-v")) {
|
|
31
|
-
console.log(currentVersion());
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
35
|
-
console.log(HELP);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
if (args[0] === "update") {
|
|
39
|
-
const latest = await checkForUpdate({ force: true, timeout: 10_000 });
|
|
40
|
-
if (!latest) {
|
|
41
|
-
console.log(`nou ${currentVersion()} is up to date.`);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
console.log(`Updating nou ${currentVersion()} → ${latest}...`);
|
|
45
|
-
if (await installUpdate(latest))
|
|
46
|
-
process.exit(0);
|
|
47
|
-
if (process.platform !== "win32")
|
|
48
|
-
console.log("If that was a permissions error, run: sudo npm install -g nou-cli@latest");
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
if (!process.stdin.isTTY) {
|
|
52
|
-
console.error("Nou needs an interactive terminal (try Windows Terminal, PowerShell, or Terminal/iTerm on macOS).");
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
p.intro(chalk.cyan(`${artWork} \n Hello I am Nou! 🌞 ${chalk.dim(`v${currentVersion()}`)}`));
|
|
56
|
-
const latest = await checkForUpdate();
|
|
57
|
-
if (latest) {
|
|
58
|
-
const update = await p.confirm({ message: `Nou ${chalk.green(latest)} is available (you have ${currentVersion()}). Update now?` });
|
|
59
|
-
if (update === true) {
|
|
60
|
-
const ok = await installUpdate(latest);
|
|
61
|
-
if (ok) {
|
|
62
|
-
p.log.success(`Updated to ${latest}. Restarting...`);
|
|
63
|
-
relaunch();
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
p.log.warn(process.platform === "win32"
|
|
67
|
-
? "The update failed. You can retry later with: npm install -g nou-cli@latest"
|
|
68
|
-
: "The update failed. If it was a permissions error, run: sudo npm install -g nou-cli@latest");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
const settings = loadSettings();
|
|
72
|
-
loadCredentials(); // keys entered inside Nou, for whatever the environment and .env leave unset
|
|
73
|
-
const borrowed = settings.reuseToolConfig ? borrowToolConfig() : [];
|
|
74
|
-
for (const b of borrowed)
|
|
75
|
-
p.log.info(`Using the ${b.provider === "anthropic" ? "Anthropic" : "OpenAI"} API key from your ${b.tool} settings.`);
|
|
76
|
-
let choice = initialModel();
|
|
77
|
-
// First run: start with a model that's already here, if there is one.
|
|
78
|
-
if (!process.env.NOU_MODEL && !settings.model) {
|
|
79
|
-
const picked = await pickStartingModel(borrowed);
|
|
80
|
-
if (picked) {
|
|
81
|
-
choice = picked;
|
|
82
|
-
saveModel(picked);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const problem = choice.provider.unavailable();
|
|
86
|
-
if (problem) {
|
|
87
|
-
p.log.warn(`${choice.provider.label} isn't set up (${problem}), falling back to local Ollama.`);
|
|
88
|
-
choice = resolveModel("ollama");
|
|
89
|
-
}
|
|
90
|
-
const ready = await ensureLocalModel(choice);
|
|
91
|
-
if (ready !== choice)
|
|
92
|
-
saveModel(ready); // picked a model that's already installed instead
|
|
93
|
-
choice = ready;
|
|
94
|
-
p.log.info(`Model: ${chalk.yellow(describe(choice))} (change it any time with /model)`);
|
|
95
|
-
await ensureProjectContext(choice);
|
|
96
|
-
const permContext = await loadPermanentContext();
|
|
97
|
-
await installFastOutput();
|
|
98
|
-
const app = render(createElement(App, { initialModel: choice, permContext }), RENDER_OPTIONS);
|
|
99
|
-
await app.waitUntilExit();
|
|
100
|
-
lsp.stopAll();
|
|
101
|
-
p.outro(chalk.cyan("Bye bye!"));
|
|
4
|
+
import { installFastWidth } from "./src/ui/fast-width.js";
|
|
5
|
+
// Answered before loading the rest, which takes most of a second.
|
|
6
|
+
if (process.argv[2] === "--version" || process.argv[2] === "-v") {
|
|
7
|
+
const { currentVersion } = await import("./src/update/index.js");
|
|
8
|
+
console.log(currentVersion());
|
|
9
|
+
process.exit(0);
|
|
102
10
|
}
|
|
103
|
-
|
|
11
|
+
// Ink's text measuring has to be swapped before Ink loads, so everything else is imported after it.
|
|
12
|
+
installFastWidth();
|
|
13
|
+
await import("./src/cli.js");
|
package/dist/src/agent/index.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
+
import { existsSync, statSync } from "fs";
|
|
2
3
|
import { toolDefs } from "../tools/index.js";
|
|
3
|
-
import { applyChange, capOutput, fileTree, isInProject, proposeEdit, proposeWrite, readForModel, runCommand, toRelative, } from "../commands/index.js";
|
|
4
|
+
import { applyChange, capOutput, fileTree, isInProject, proposeEdit, proposeEdits, proposeWrite, readForModel, runCommand, toRelative, } from "../commands/index.js";
|
|
4
5
|
import { describe, keyHint } from "../providers/index.js";
|
|
5
6
|
import { newCallId } from "../providers/ids.js";
|
|
6
7
|
import { globFiles, grep } from "../search/index.js";
|
|
7
8
|
/** Model requests per user message before stopping; real tasks (explore, edit, test, fix) take a few dozen. */
|
|
8
9
|
const MAX_STEPS = 60;
|
|
10
|
+
/** Tries per model request when it fails for a passing reason (see withRetries). */
|
|
11
|
+
const RETRIES = 6;
|
|
12
|
+
/** Errors worth retrying: overloaded, rate limited, a server error, a dropped or timed-out connection. */
|
|
13
|
+
function transient(error) {
|
|
14
|
+
const status = Number(error?.status ?? error?.statusCode);
|
|
15
|
+
if (status === 408 || status === 409 || status === 429 || status >= 500)
|
|
16
|
+
return true;
|
|
17
|
+
if (status >= 400)
|
|
18
|
+
return false;
|
|
19
|
+
const text = String(error?.message ?? error);
|
|
20
|
+
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|Invalid URL|api key|unauthori[sz]ed/i.test(text))
|
|
21
|
+
return false;
|
|
22
|
+
return /overloaded|rate.?limit|too many requests|ECONNRESET|socket hang up|ETIMEDOUT|timed? ?out|EPIPE|network|premature close|terminated|\b(429|5\d\d)\b/i.test(text);
|
|
23
|
+
}
|
|
9
24
|
/** Messages kept word for word when older ones are summarized. */
|
|
10
25
|
const KEEP_RECENT = 8;
|
|
11
26
|
const TRIMMED_OUTPUT = "\n[... trimmed to save context; run the tool again if you need it]";
|
|
@@ -24,6 +39,13 @@ How to work:
|
|
|
24
39
|
4. Keep going until the task is done. The user approves each edit and command as it happens, so don't stop to ask permission between steps; ask only when the request is unclear or you need a decision.
|
|
25
40
|
5. End with a short summary of what you changed and why. When the user only asks a question, answer it without editing anything.
|
|
26
41
|
|
|
42
|
+
- Make independent calls together: several reads or searches in one response run at the same time.
|
|
43
|
+
- Use grep, glob and read_file to look around, not run_command with grep, find or cat. Several edits to one file go in one multi_edit.
|
|
44
|
+
- If an edit fails twice, read the file again before the next try.
|
|
45
|
+
- For work with three or more steps, keep a plan with update_plan and update it as you go.
|
|
46
|
+
- Don't commit, push, install packages or delete files unless the user asked. Don't add comments the code didn't need.
|
|
47
|
+
- Before saying something works, check it (run the tests or the code). If you couldn't, say so.
|
|
48
|
+
- When the user asks where something is, or to show, open or find something, search for it (grep, glob), read enough to be sure, then call open_file on the exact place (with line and end_line) so it opens in their code view, and answer in a sentence or two with path:line.
|
|
27
49
|
- If the user rejects an edit or command, don't retry it: ask what they would like instead.
|
|
28
50
|
- Keep replies short. Refer to code as path:line.
|
|
29
51
|
|
|
@@ -66,6 +88,11 @@ export class Agent {
|
|
|
66
88
|
this.hooks = hooks;
|
|
67
89
|
}
|
|
68
90
|
configure(options) {
|
|
91
|
+
// A different system prompt or tool list (an MCP server connected, a plugin switched) changes what the
|
|
92
|
+
// earlier thinking was written against: newer Claude models reject replaying it, so it's dropped.
|
|
93
|
+
const toolsChanged = options.extraTools && options.extraTools.map((t) => t.definition.name).join() !== this.extraTools.map((t) => t.definition.name).join();
|
|
94
|
+
if (this.messages.length && ((options.system !== undefined && options.system !== this.system) || toolsChanged))
|
|
95
|
+
this.forgetThinking();
|
|
69
96
|
if (options.system !== undefined)
|
|
70
97
|
this.system = options.system;
|
|
71
98
|
if (options.extraTools)
|
|
@@ -123,6 +150,8 @@ export class Agent {
|
|
|
123
150
|
trimmed++;
|
|
124
151
|
}
|
|
125
152
|
}
|
|
153
|
+
if (trimmed)
|
|
154
|
+
this.forgetThinking();
|
|
126
155
|
if (!force && this.requestSize() <= budget * 0.8) {
|
|
127
156
|
this.hooks.onEntry({ kind: "info", text: `Trimmed ${trimmed} old tool outputs to fit the model's context.` });
|
|
128
157
|
return true;
|
|
@@ -140,6 +169,7 @@ export class Agent {
|
|
|
140
169
|
throw error;
|
|
141
170
|
summary = `(The earlier conversation was dropped to fit the context; summarizing it failed: ${error.message})`;
|
|
142
171
|
}
|
|
172
|
+
this.forgetThinking();
|
|
143
173
|
const intro = `[Summary of the conversation so far]\n${summary}`;
|
|
144
174
|
const rest = this.messages.slice(split);
|
|
145
175
|
const first = rest[0];
|
|
@@ -152,6 +182,26 @@ export class Agent {
|
|
|
152
182
|
});
|
|
153
183
|
return true;
|
|
154
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Drops Claude's thinking blocks from the history. Newer Claude models reject a conversation whose
|
|
187
|
+
* earlier turns were edited while it still carries their thinking blocks (they're tied to the exact
|
|
188
|
+
* history they were written in), and trimming or summarizing is such an edit. Without them the model
|
|
189
|
+
* carries on from the visible text and tool calls, which is what the trimmed history holds anyway.
|
|
190
|
+
*/
|
|
191
|
+
forgetThinking() {
|
|
192
|
+
this.messages = this.messages.map((m) => {
|
|
193
|
+
if (m.role !== "assistant" || m.raw?.provider !== "anthropic" || !Array.isArray(m.raw.data))
|
|
194
|
+
return m;
|
|
195
|
+
const kept = m.raw.data.filter((b) => b.type !== "thinking" && b.type !== "redacted_thinking");
|
|
196
|
+
if (kept.length === m.raw.data.length)
|
|
197
|
+
return m;
|
|
198
|
+
if (kept.length)
|
|
199
|
+
return { ...m, raw: { ...m.raw, data: kept } };
|
|
200
|
+
const plain = { ...m };
|
|
201
|
+
delete plain.raw;
|
|
202
|
+
return plain;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
155
205
|
/**
|
|
156
206
|
* Where to cut the history for a summary: as late as possible while keeping the last few messages, and
|
|
157
207
|
* only before a user or assistant message, so no tool call is separated from its result.
|
|
@@ -198,22 +248,26 @@ export class Agent {
|
|
|
198
248
|
this.controller = new AbortController();
|
|
199
249
|
const { signal } = this.controller;
|
|
200
250
|
try {
|
|
251
|
+
let cutOff = 0;
|
|
201
252
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
202
253
|
if (this.requestSize() > this.contextBudget())
|
|
203
254
|
await this.compact(false, signal);
|
|
204
255
|
this.hooks.onBusy(`Pondering with ${describe(this.choice)}...`);
|
|
205
256
|
let streamed = "";
|
|
206
|
-
const result = await this.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
257
|
+
const result = await this.withRetries(signal, () => {
|
|
258
|
+
streamed = "";
|
|
259
|
+
return this.choice.provider.chat({
|
|
260
|
+
model: this.choice.model,
|
|
261
|
+
system: this.system,
|
|
262
|
+
messages: this.messages,
|
|
263
|
+
tools: this.tools,
|
|
264
|
+
signal,
|
|
265
|
+
...(this.effort ? { effort: this.effort } : {}),
|
|
266
|
+
onText: (delta) => {
|
|
267
|
+
streamed += delta;
|
|
268
|
+
this.hooks.onStream(streamed);
|
|
269
|
+
},
|
|
270
|
+
});
|
|
217
271
|
});
|
|
218
272
|
this.hooks.onStream("");
|
|
219
273
|
// Tool-call ids must be unique across the conversation (results are matched by id, and some APIs
|
|
@@ -241,30 +295,51 @@ export class Agent {
|
|
|
241
295
|
return;
|
|
242
296
|
}
|
|
243
297
|
if (result.stop === "max_tokens") {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
298
|
+
// The reply ran out of room mid tool call (usually a whole file in one write): tell the model and
|
|
299
|
+
// let it try in smaller pieces, a couple of times, before giving up.
|
|
300
|
+
if (++cutOff > 2) {
|
|
301
|
+
this.hooks.onEntry({ kind: "error", text: "The response was cut off before its tool call finished, again. Try asking for a smaller change." });
|
|
302
|
+
this.dropUnansweredCalls();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
for (const call of result.toolCalls) {
|
|
306
|
+
this.messages.push({ role: "tool", callId: call.id, name: call.name, isError: true,
|
|
307
|
+
content: "Not run: your response hit the output limit before this call was complete. Make the change in smaller steps (edit_file or multi_edit with only the lines that change, not the whole file)." });
|
|
308
|
+
}
|
|
309
|
+
continue;
|
|
247
310
|
}
|
|
248
311
|
if (result.toolCalls.length === 0)
|
|
249
312
|
return;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
313
|
+
// Calls that only look (reads, searches) run together; the rest run one at a time, in order,
|
|
314
|
+
// since they may ask you first or change what a later call sees. Results keep the calls' order.
|
|
315
|
+
const calls = result.toolCalls;
|
|
316
|
+
for (let i = 0; i < calls.length && !signal.aborted;) {
|
|
317
|
+
let j = i + 1;
|
|
318
|
+
if (this.onlyLooks(calls[i]))
|
|
319
|
+
while (j < calls.length && this.onlyLooks(calls[j]))
|
|
320
|
+
j++;
|
|
321
|
+
const batch = calls.slice(i, j);
|
|
322
|
+
this.hooks.onBusy(batch.length > 1 ? `Running ${batch.length} tools...` : `Running ${batch[0].name}...`);
|
|
323
|
+
const outputs = await Promise.all(batch.map(async (call) => {
|
|
324
|
+
const problem = invalidArgs(call, this.tools);
|
|
325
|
+
return problem ? { error: `Error: ${problem}` } : { output: capOutput(await this.runTool(call)) };
|
|
326
|
+
}));
|
|
327
|
+
batch.forEach((call, k) => {
|
|
328
|
+
const out = outputs[k];
|
|
329
|
+
this.messages.push({
|
|
330
|
+
role: "tool",
|
|
331
|
+
callId: call.id,
|
|
332
|
+
name: call.name,
|
|
333
|
+
content: "error" in out ? out.error : out.output,
|
|
334
|
+
...("error" in out ? { isError: true } : {}),
|
|
335
|
+
});
|
|
262
336
|
});
|
|
337
|
+
i = j;
|
|
263
338
|
}
|
|
264
339
|
if (signal.aborted)
|
|
265
340
|
throw new Error("aborted");
|
|
266
341
|
}
|
|
267
|
-
this.hooks.onEntry({ kind: "error", text: `
|
|
342
|
+
this.hooks.onEntry({ kind: "error", text: `Paused after ${MAX_STEPS} steps so it doesn't run away. Say "continue" to let it carry on.` });
|
|
268
343
|
}
|
|
269
344
|
catch (error) {
|
|
270
345
|
this.hooks.onStream("");
|
|
@@ -296,6 +371,40 @@ export class Agent {
|
|
|
296
371
|
}
|
|
297
372
|
}
|
|
298
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Runs a model request again when it failed for a passing reason (overloaded, rate limited, a server
|
|
376
|
+
* error, a dropped connection), waiting longer each time. Setup problems (a bad key, a URL that doesn't
|
|
377
|
+
* answer) fail straight away: waiting won't fix them, and the app shows where to.
|
|
378
|
+
*/
|
|
379
|
+
async withRetries(signal, request) {
|
|
380
|
+
for (let attempt = 1;; attempt++) {
|
|
381
|
+
try {
|
|
382
|
+
return await request();
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
if (signal.aborted || attempt >= RETRIES || !transient(error))
|
|
386
|
+
throw error;
|
|
387
|
+
const after = Number(error?.headers?.get?.("retry-after") ?? error?.headers?.["retry-after"]);
|
|
388
|
+
const wait = Math.min(30_000, after > 0 ? after * 1000 : 1000 * 2 ** (attempt - 1) + Math.random() * 500);
|
|
389
|
+
this.hooks.onStream("");
|
|
390
|
+
this.hooks.onBusy(`${describe(this.choice)}: ${String(error?.message ?? error).slice(0, 60)}. Retrying in ${Math.ceil(wait / 1000)}s (${attempt}/${RETRIES - 1})...`);
|
|
391
|
+
await new Promise((resolve) => {
|
|
392
|
+
const timer = setTimeout(resolve, wait);
|
|
393
|
+
signal.addEventListener("abort", () => (clearTimeout(timer), resolve()), { once: true });
|
|
394
|
+
});
|
|
395
|
+
if (signal.aborted)
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/** Tools that only read, which can run alongside each other. */
|
|
401
|
+
onlyLooks(call) {
|
|
402
|
+
if (["grep", "glob", "list_files", "get_file_tree"].includes(call.name))
|
|
403
|
+
return true;
|
|
404
|
+
if (call.name === "read_file")
|
|
405
|
+
return typeof call.args.path === "string" && isInProject(call.args.path);
|
|
406
|
+
return this.extraTools.some((t) => t.definition.name === call.name && t.readOnly);
|
|
407
|
+
}
|
|
299
408
|
async runTool(call) {
|
|
300
409
|
const { name, args } = call;
|
|
301
410
|
try {
|
|
@@ -309,11 +418,25 @@ export class Agent {
|
|
|
309
418
|
}
|
|
310
419
|
}
|
|
311
420
|
else {
|
|
312
|
-
|
|
421
|
+
const from = Number(args.offset) > 1 ? Math.floor(Number(args.offset)) : undefined;
|
|
422
|
+
const count = Number(args.limit) > 0 ? Math.floor(Number(args.limit)) : undefined;
|
|
423
|
+
this.hooks.onRead(toRelative(args.path), from ?? (count ? 1 : undefined), count ? (from ?? 1) + count - 1 : undefined);
|
|
313
424
|
}
|
|
314
425
|
this.hooks.onEntry({ kind: "tool", text: `Read ${args.path}` });
|
|
315
426
|
return await readForModel(args.path, args.offset, args.limit);
|
|
316
427
|
}
|
|
428
|
+
case "open_file": {
|
|
429
|
+
const file = toRelative(String(args.path));
|
|
430
|
+
if (!isInProject(file))
|
|
431
|
+
return "Error: only files in the project can be shown.";
|
|
432
|
+
if (!existsSync(file) || !statSync(file).isFile())
|
|
433
|
+
return `Error: ${file} is not a file. Find it with glob first.`;
|
|
434
|
+
const line = Number(args.line) > 0 ? Math.floor(Number(args.line)) : undefined;
|
|
435
|
+
const to = line && Number(args.end_line) >= line ? Math.floor(Number(args.end_line)) : line;
|
|
436
|
+
this.hooks.onOpen?.(file, line, to);
|
|
437
|
+
this.hooks.onEntry({ kind: "tool", text: `Opened ${file}${line ? `:${line}${to && to !== line ? `-${to}` : ""}` : ""}` });
|
|
438
|
+
return this.hooks.onOpen ? "It is now open in the user's code view." : "This interface can't show files; give the user the path instead.";
|
|
439
|
+
}
|
|
317
440
|
case "list_files":
|
|
318
441
|
case "get_file_tree": {
|
|
319
442
|
this.hooks.onEntry({ kind: "tool", text: `Listed ${args.path ?? "."}` });
|
|
@@ -335,12 +458,21 @@ export class Agent {
|
|
|
335
458
|
return globFiles(String(args.pattern), typeof args.path === "string" ? args.path : ".");
|
|
336
459
|
}
|
|
337
460
|
case "edit_file":
|
|
461
|
+
case "multi_edit":
|
|
338
462
|
case "write_file": {
|
|
463
|
+
if (name === "multi_edit" && (!Array.isArray(args.edits) || !args.edits.length))
|
|
464
|
+
return "Error: edits must be a list of {old_string, new_string}.";
|
|
339
465
|
const change = name === "edit_file"
|
|
340
466
|
? await proposeEdit(args.path, args.old_string, args.new_string, args.replace_all === true)
|
|
341
|
-
: await proposeWrite(args.path, args.content ?? "");
|
|
467
|
+
: name === "multi_edit" ? await proposeEdits(args.path, args.edits) : await proposeWrite(args.path, args.content ?? "");
|
|
342
468
|
return await this.reviewChange(change);
|
|
343
469
|
}
|
|
470
|
+
case "update_plan": {
|
|
471
|
+
const steps = Array.isArray(args.steps) ? args.steps : [];
|
|
472
|
+
const mark = { done: "✓", in_progress: "▸", pending: "○" };
|
|
473
|
+
this.hooks.onEntry({ kind: "tool", text: `Plan:\n${steps.map((st) => `${mark[st.status] ?? "○"} ${st.step}`).join("\n")}` });
|
|
474
|
+
return "Plan updated.";
|
|
475
|
+
}
|
|
344
476
|
case "run_command": {
|
|
345
477
|
const ok = await this.hooks.approve({ kind: "command", command: args.command });
|
|
346
478
|
if (!ok) {
|
|
@@ -348,7 +480,9 @@ export class Agent {
|
|
|
348
480
|
return "User denied permission to run this command.";
|
|
349
481
|
}
|
|
350
482
|
this.hooks.onEntry({ kind: "tool", text: `Ran: ${args.command}` });
|
|
351
|
-
|
|
483
|
+
// Timeouts are in seconds; a model that sends milliseconds still gets a sensible limit.
|
|
484
|
+
const seconds = Number(args.timeout) > 0 ? Number(args.timeout) : 0;
|
|
485
|
+
const output = await runCommand(args.command, seconds ? (seconds > 3600 ? seconds : seconds * 1000) : undefined, this.controller?.signal);
|
|
352
486
|
return output.trim() || "(no output)";
|
|
353
487
|
}
|
|
354
488
|
default: {
|
|
@@ -378,10 +512,12 @@ export class Agent {
|
|
|
378
512
|
return "User rejected this change. Ask them what they would like instead.";
|
|
379
513
|
}
|
|
380
514
|
const result = await applyChange(change);
|
|
381
|
-
this.hooks.onChange(change, "applied", this.turnId);
|
|
382
515
|
this.hooks.onEntry({ kind: "tool", text: result });
|
|
383
|
-
if (result.startsWith("Failed"))
|
|
516
|
+
if (result.startsWith("Failed")) {
|
|
517
|
+
this.hooks.onChange(change, "rejected", this.turnId);
|
|
384
518
|
return result;
|
|
519
|
+
}
|
|
520
|
+
this.hooks.onChange(change, "applied", this.turnId);
|
|
385
521
|
this.hooks.onBusy(`Checking ${change.path} for problems...`);
|
|
386
522
|
const problems = await this.hooks.checkChange?.(change).catch(() => null);
|
|
387
523
|
if (problems)
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The whole command line program; index.ts loads it once the environment is ready.
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { artWork } from "./util/index.js";
|
|
7
|
+
import { ensureProjectContext, loadPermanentContext } from "./context/index.js";
|
|
8
|
+
import { describe, initialModel, resolveModel, saveModel } from "./providers/index.js";
|
|
9
|
+
import { isTrustedFolder, loadSettings, saveSettings } from "./config/index.js";
|
|
10
|
+
import { loadCredentials } from "./config/credentials.js";
|
|
11
|
+
import { borrowToolConfig, ensureLocalModel, pickStartingModel } from "./setup/index.js";
|
|
12
|
+
import { checkForUpdate, currentVersion, installUpdate, relaunch } from "./update/index.js";
|
|
13
|
+
import { lsp } from "./lsp/index.js";
|
|
14
|
+
import { App } from "./ui/App.js";
|
|
15
|
+
import { installFastOutput } from "./ui/fast-output.js";
|
|
16
|
+
import { RENDER_OPTIONS } from "./ui/render-options.js";
|
|
17
|
+
const HELP = `nou ${currentVersion()}: an AI coding assistant for your terminal
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
nou open Nou in the current folder
|
|
21
|
+
nou update update to the latest version
|
|
22
|
+
nou --version print the version
|
|
23
|
+
nou --help show this help
|
|
24
|
+
|
|
25
|
+
Settings live in ~/.nou/config.json (or press s inside Nou); keys you enter there, or with /connect, in ~/.nou/credentials.json. Set NOU_NO_UPDATE_CHECK=1 to skip update checks.`;
|
|
26
|
+
async function main() {
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
29
|
+
console.log(currentVersion());
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
33
|
+
console.log(HELP);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (args[0] === "update") {
|
|
37
|
+
const latest = await checkForUpdate({ force: true, timeout: 10_000 });
|
|
38
|
+
if (!latest) {
|
|
39
|
+
console.log(`nou ${currentVersion()} is up to date.`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(`Updating nou ${currentVersion()} → ${latest}...`);
|
|
43
|
+
if (await installUpdate(latest))
|
|
44
|
+
process.exit(0);
|
|
45
|
+
if (process.platform !== "win32")
|
|
46
|
+
console.log("If that was a permissions error, run: sudo npm install -g nou-cli@latest");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (!process.stdin.isTTY) {
|
|
50
|
+
console.error("Nou needs an interactive terminal (try Windows Terminal, PowerShell, or Terminal/iTerm on macOS).");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
p.intro(chalk.cyan(`${artWork} \n Hello I am Nou! 🌞 ${chalk.dim(`v${currentVersion()}`)}`));
|
|
54
|
+
// A folder's files can run things (its MCP servers, plugins, the commands Nou is asked to run), so a
|
|
55
|
+
// folder is trusted once, like Claude Code and VS Code ask.
|
|
56
|
+
if (!isTrustedFolder()) {
|
|
57
|
+
const here = process.cwd();
|
|
58
|
+
p.log.warn(`Nou will read the files in ${chalk.bold(here)} and can run its MCP servers and plugins, and commands you approve.`);
|
|
59
|
+
const trust = await p.confirm({ message: "Do you trust the files in this folder?" });
|
|
60
|
+
if (trust !== true) {
|
|
61
|
+
p.outro("OK, not opening it. Run nou in a folder you trust.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
saveSettings({ trustedFolders: [...loadSettings().trustedFolders, here] });
|
|
65
|
+
}
|
|
66
|
+
const latest = await checkForUpdate();
|
|
67
|
+
if (latest) {
|
|
68
|
+
const update = await p.confirm({ message: `Nou ${chalk.green(latest)} is available (you have ${currentVersion()}). Update now?` });
|
|
69
|
+
if (update === true) {
|
|
70
|
+
const ok = await installUpdate(latest);
|
|
71
|
+
if (ok) {
|
|
72
|
+
p.log.success(`Updated to ${latest}. Restarting...`);
|
|
73
|
+
relaunch();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
p.log.warn(process.platform === "win32"
|
|
77
|
+
? "The update failed. You can retry later with: npm install -g nou-cli@latest"
|
|
78
|
+
: "The update failed. If it was a permissions error, run: sudo npm install -g nou-cli@latest");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const settings = loadSettings();
|
|
82
|
+
loadCredentials(); // keys entered inside Nou, for whatever the environment and .env leave unset
|
|
83
|
+
const borrowed = settings.reuseToolConfig ? borrowToolConfig() : [];
|
|
84
|
+
for (const b of borrowed)
|
|
85
|
+
p.log.info(`Using the ${b.provider === "anthropic" ? "Anthropic" : "OpenAI"} API key from your ${b.tool} settings.`);
|
|
86
|
+
let choice = initialModel();
|
|
87
|
+
// First run: start with a model that's already here, if there is one.
|
|
88
|
+
if (!process.env.NOU_MODEL && !settings.model) {
|
|
89
|
+
const picked = await pickStartingModel(borrowed);
|
|
90
|
+
if (picked) {
|
|
91
|
+
choice = picked;
|
|
92
|
+
saveModel(picked);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const problem = choice.provider.unavailable();
|
|
96
|
+
if (problem) {
|
|
97
|
+
p.log.warn(`${choice.provider.label} isn't set up (${problem}), falling back to local Ollama.`);
|
|
98
|
+
choice = resolveModel("ollama");
|
|
99
|
+
}
|
|
100
|
+
const ready = await ensureLocalModel(choice);
|
|
101
|
+
if (ready !== choice)
|
|
102
|
+
saveModel(ready); // picked a model that's already installed instead
|
|
103
|
+
choice = ready;
|
|
104
|
+
p.log.info(`Model: ${chalk.yellow(describe(choice))} (change it any time with /model)`);
|
|
105
|
+
// NOU.md is for Nou's own engine; the others read their own files (CLAUDE.md, AGENTS.md).
|
|
106
|
+
if ((settings.engine ?? "nou") === "nou")
|
|
107
|
+
await ensureProjectContext(choice);
|
|
108
|
+
const permContext = await loadPermanentContext();
|
|
109
|
+
await installFastOutput();
|
|
110
|
+
const app = render(createElement(App, { initialModel: choice, permContext }), RENDER_OPTIONS);
|
|
111
|
+
await app.waitUntilExit();
|
|
112
|
+
lsp.stopAll();
|
|
113
|
+
p.outro(chalk.cyan("Bye bye!"));
|
|
114
|
+
}
|
|
115
|
+
main().catch(console.error);
|