nou-cli 0.3.0 → 0.5.0
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/.env.example +1 -0
- package/README.md +26 -8
- package/dist/index.js +4 -5
- package/dist/src/agent/index.js +3 -2
- package/dist/src/config/index.js +2 -1
- package/dist/src/env.js +3 -0
- package/dist/src/providers/index.js +16 -0
- package/dist/src/providers/openai.js +7 -4
- package/dist/src/syntax/index.js +11 -2
- package/dist/src/ui/App.js +300 -70
- package/dist/src/ui/Chat.js +105 -49
- package/dist/src/ui/ChatInput.js +11 -2
- package/dist/src/ui/CodeView.js +42 -23
- package/dist/src/ui/Editor.js +471 -99
- package/dist/src/ui/FileTree.js +74 -42
- package/dist/src/ui/Header.js +3 -2
- package/dist/src/ui/ProblemsView.js +2 -2
- package/dist/src/ui/Settings.js +2 -4
- package/dist/src/ui/clipboard.js +59 -0
- package/dist/src/ui/fast-output.js +250 -0
- package/dist/src/ui/mouse.js +8 -3
- package/dist/src/ui/render-options.js +11 -0
- package/dist/src/ui/scroll.js +30 -0
- package/dist/src/ui/text.js +155 -0
- package/package.json +3 -2
package/.env.example
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
# A LiteLLM proxy, or any hosted OpenAI-compatible gateway (full cloud context, unlike NOU_BASE_URL):
|
|
30
30
|
# LITELLM_BASE_URL=http://localhost:4000/v1
|
|
31
31
|
# LITELLM_API_KEY=
|
|
32
|
+
# (LITELLM_PROXY_API_BASE and LITELLM_PROXY_API_KEY, the names LiteLLM's SDK uses, work too.)
|
|
32
33
|
# NOU_MODEL=litellm:<a model name from your proxy>
|
|
33
34
|
# Or skip this file: /connect <provider> inside Nou saves keys to ~/.nou/credentials.json.
|
|
34
35
|
|
package/README.md
CHANGED
|
@@ -76,22 +76,40 @@ Every change is reviewed like a code review:
|
|
|
76
76
|
|
|
77
77
|
| Key | Does |
|
|
78
78
|
| --- | --- |
|
|
79
|
-
| 🖱️ Mouse | wheel scrolls the pane under the pointer · click a
|
|
80
|
-
| <kbd>
|
|
79
|
+
| 🖱️ 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 · **click in the code to edit right there**, 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
|
+
| <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>/</kbd> | start a command from any pane |
|
|
81
82
|
| <kbd>Tab</kbd> | switch between files, code and chat |
|
|
83
|
+
| <kbd>Shift</kbd>+<kbd>Tab</kbd> | ask → auto-edit (edits apply without asking) → auto (commands too); everything stays undoable |
|
|
82
84
|
| <kbd>Ctrl</kbd>+<kbd>P</kbd> | fuzzy-find a file |
|
|
83
|
-
| <kbd>e</kbd> | edit the open file (
|
|
85
|
+
| <kbd>e</kbd> or a click | edit the open file (see the editor keys below) |
|
|
84
86
|
| <kbd>p</kbd> | Problems panel: every error and warning, <kbd>Enter</kbd> jumps to it |
|
|
85
87
|
| <kbd>u</kbd> | undo Nou's last edit |
|
|
86
88
|
| <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 |
|
|
87
89
|
| <kbd>c</kbd> | in the file tree: show only changed files |
|
|
88
|
-
| <kbd>f</kbd> | follow mode: jump to files as Nou reads and edits them |
|
|
90
|
+
| <kbd>f</kbd> | follow mode (off by default): jump to files as Nou reads and edits them |
|
|
91
|
+
| <kbd><</kbd> / <kbd>></kbd> / <kbd>=</kbd> | make the focused pane narrower / wider / reset the layout |
|
|
92
|
+
| <kbd>m</kbd> | mouse on/off, so you can select and copy text in the terminal |
|
|
89
93
|
| <kbd>s</kbd> | settings: model, preferences, plugins |
|
|
90
|
-
| <kbd>y</kbd> / <kbd>n</kbd> | accept / reject a proposed edit or command |
|
|
94
|
+
| <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) |
|
|
91
95
|
| <kbd>Esc</kbd> | stop Nou · close a diff · back to chat |
|
|
92
96
|
| <kbd>Ctrl</kbd>+<kbd>C</kbd> | stop Nou while it works · press twice to quit |
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
In the editor:
|
|
99
|
+
|
|
100
|
+
| Key | Does |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| <kbd>Shift</kbd>+arrows, <kbd>Shift</kbd>+<kbd>Home</kbd>/<kbd>End</kbd>, mouse drag | select · <kbd>Ctrl</kbd>+<kbd>A</kbd> select all |
|
|
103
|
+
| <kbd>Ctrl</kbd>+<kbd>C</kbd> / <kbd>X</kbd> / <kbd>V</kbd> | copy / cut / paste (the whole line when nothing is selected); your terminal's own paste works too |
|
|
104
|
+
| <kbd>Ctrl</kbd>+<kbd>←</kbd>/<kbd>→</kbd>, <kbd>Ctrl</kbd>+<kbd>Backspace</kbd> or <kbd>Ctrl</kbd>+<kbd>W</kbd> | move by word, delete a word · <kbd>Ctrl</kbd>+<kbd>Home</kbd>/<kbd>End</kbd> start / end of the file |
|
|
105
|
+
| <kbd>Ctrl</kbd>+<kbd>F</kbd> · <kbd>Ctrl</kbd>+<kbd>L</kbd> | find (<kbd>Enter</kbd>/<kbd>↓</kbd> next, <kbd>Shift</kbd>+<kbd>Enter</kbd>/<kbd>↑</kbd> previous) · go to line |
|
|
106
|
+
| <kbd>Ctrl</kbd>+<kbd>/</kbd> · <kbd>Ctrl</kbd>+<kbd>D</kbd> · <kbd>Alt</kbd>+<kbd>↑</kbd>/<kbd>↓</kbd> | comment the line or selection · duplicate it · move it up / down |
|
|
107
|
+
| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | indent / outdent (whole lines with a selection) · <kbd>Enter</kbd> keeps the indent, one more after `{`, `(`, `[` |
|
|
108
|
+
| <kbd>Ctrl</kbd>+<kbd>S</kbd> · <kbd>Ctrl</kbd>+<kbd>Z</kbd>/<kbd>Y</kbd> | save (auto-save is on by default) · undo / redo |
|
|
109
|
+
| <kbd>Ctrl</kbd>+<kbd>K</kbd> · <kbd>Ctrl</kbd>+<kbd>G</kbd> | hover info · go to definition (with a language server) |
|
|
110
|
+
| <kbd>Esc</kbd> | clear the selection, or leave the editor |
|
|
111
|
+
|
|
112
|
+
Chat commands: `/settings` · `/model [provider:model]` · `/models [provider]` · `/pull <model>` · `/engine [name]` · `/auto [ask|auto-edit|auto]` · `/copy [code|file]` · `/undo` · `/rewind` · `/history` · `/diff [file or n]` · `/sessions` · `/problems` · `/compact` · `/clear` · `/help`
|
|
95
113
|
|
|
96
114
|
## 🧠 Models
|
|
97
115
|
|
|
@@ -108,9 +126,9 @@ If you haven't set a key, Nou uses the API key from Claude Code (`~/.claude/sett
|
|
|
108
126
|
| 🖥️ LM Studio | 🟢 local | start its local server |
|
|
109
127
|
| ⚙️ llama.cpp server | 🟢 local | start `llama-server` |
|
|
110
128
|
| 🔧 Any OpenAI-compatible server | 🟢 local | `NOU_BASE_URL`, optional `NOU_API_KEY` |
|
|
111
|
-
| 🚅 LiteLLM proxy or any hosted gateway | ☁️ cloud | `/connect litellm`, or `LITELLM_BASE_URL` + `LITELLM_API_KEY` |
|
|
129
|
+
| 🚅 LiteLLM proxy or any hosted gateway | ☁️ cloud | `/connect litellm`, or `LITELLM_BASE_URL` + `LITELLM_API_KEY` (LiteLLM's `LITELLM_PROXY_API_BASE` + `LITELLM_PROXY_API_KEY` work too) |
|
|
112
130
|
| 🟣 Anthropic (Claude) | ☁️ cloud | `ANTHROPIC_API_KEY` |
|
|
113
|
-
| ⚪ OpenAI | ☁️ cloud | `OPENAI_API_KEY` |
|
|
131
|
+
| ⚪ OpenAI | ☁️ cloud | `OPENAI_API_KEY`, optional `OPENAI_BASE_URL` |
|
|
114
132
|
| 🔷 Google Gemini | ☁️ cloud | `GEMINI_API_KEY` |
|
|
115
133
|
| 🔀 OpenRouter | ☁️ cloud | `OPENROUTER_API_KEY` |
|
|
116
134
|
| ⚡ Groq | ☁️ cloud | `GROQ_API_KEY` |
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import { borrowToolConfig, ensureLocalModel, pickStartingModel } from "./src/set
|
|
|
14
14
|
import { checkForUpdate, currentVersion, installUpdate, relaunch } from "./src/update/index.js";
|
|
15
15
|
import { lsp } from "./src/lsp/index.js";
|
|
16
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";
|
|
17
19
|
const HELP = `nou ${currentVersion()}: an AI coding assistant for your terminal
|
|
18
20
|
|
|
19
21
|
Usage:
|
|
@@ -92,11 +94,8 @@ async function main() {
|
|
|
92
94
|
p.log.info(`Model: ${chalk.yellow(describe(choice))} (change it any time with /model)`);
|
|
93
95
|
await ensureProjectContext(choice);
|
|
94
96
|
const permContext = await loadPermanentContext();
|
|
95
|
-
|
|
96
|
-
const app = render(createElement(App, { initialModel: choice, permContext }),
|
|
97
|
-
alternateScreen: true,
|
|
98
|
-
exitOnCtrlC: false,
|
|
99
|
-
});
|
|
97
|
+
await installFastOutput();
|
|
98
|
+
const app = render(createElement(App, { initialModel: choice, permContext }), RENDER_OPTIONS);
|
|
100
99
|
await app.waitUntilExit();
|
|
101
100
|
lsp.stopAll();
|
|
102
101
|
p.outro(chalk.cyan("Bye bye!"));
|
package/dist/src/agent/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import { toolDefs } from "../tools/index.js";
|
|
3
3
|
import { applyChange, capOutput, fileTree, isInProject, proposeEdit, proposeWrite, readForModel, runCommand, toRelative, } from "../commands/index.js";
|
|
4
|
-
import { describe } from "../providers/index.js";
|
|
4
|
+
import { describe, keyHint } from "../providers/index.js";
|
|
5
5
|
import { newCallId } from "../providers/ids.js";
|
|
6
6
|
import { globFiles, grep } from "../search/index.js";
|
|
7
7
|
/** Model requests per user message before stopping; real tasks (explore, edit, test, fix) take a few dozen. */
|
|
@@ -272,7 +272,8 @@ export class Agent {
|
|
|
272
272
|
this.hooks.onEntry({ kind: "info", text: "Cancelled." });
|
|
273
273
|
}
|
|
274
274
|
else {
|
|
275
|
-
|
|
275
|
+
const hint = keyHint(this.choice.provider, String(error.message));
|
|
276
|
+
this.hooks.onEntry({ kind: "error", text: `${describe(this.choice)}: ${error.message}${hint ? `\n${hint}` : ""}` });
|
|
276
277
|
}
|
|
277
278
|
this.dropUnansweredCalls();
|
|
278
279
|
}
|
package/dist/src/config/index.js
CHANGED
|
@@ -9,13 +9,14 @@ export const DEFAULT_SETTINGS = {
|
|
|
9
9
|
model: "",
|
|
10
10
|
engine: "nou",
|
|
11
11
|
effort: "high",
|
|
12
|
-
followMode:
|
|
12
|
+
followMode: false,
|
|
13
13
|
autoApproveEdits: false,
|
|
14
14
|
autoApproveCommands: false,
|
|
15
15
|
gitContext: true,
|
|
16
16
|
autoSave: "delay",
|
|
17
17
|
mouse: true,
|
|
18
18
|
reuseToolConfig: true,
|
|
19
|
+
paneWidths: null,
|
|
19
20
|
plugins: {},
|
|
20
21
|
trustedPlugins: {},
|
|
21
22
|
};
|
package/dist/src/env.js
CHANGED
|
@@ -7,6 +7,9 @@ if (major < 22 || (major === 22 && minor < 12)) {
|
|
|
7
7
|
console.error(`Nou needs Node.js 22.12 or newer (you have ${process.versions.node}). Get it from https://nodejs.org`);
|
|
8
8
|
process.exit(1);
|
|
9
9
|
}
|
|
10
|
+
// React and Ink pick their build from NODE_ENV when they load: without this every render runs React's
|
|
11
|
+
// development build, which is several times slower and checks every update.
|
|
12
|
+
process.env.NODE_ENV ??= "production";
|
|
10
13
|
// Load the project's .env (API keys, base URLs). Providers read their settings when they load,
|
|
11
14
|
// so loading .env later would be too late for them.
|
|
12
15
|
if (existsSync(".env")) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadSettings, saveSettings } from "../config/index.js";
|
|
2
|
+
import { originOf } from "../config/origins.js";
|
|
2
3
|
import { anthropicProvider } from "./anthropic.js";
|
|
3
4
|
import { ollamaProvider } from "./ollama.js";
|
|
4
5
|
import { OPENAI_COMPATIBLE, openAICompatibleProvider } from "./openai.js";
|
|
@@ -24,6 +25,21 @@ export function resolveModel(spec) {
|
|
|
24
25
|
return { provider: providers[0], model: trimmed };
|
|
25
26
|
}
|
|
26
27
|
export const describe = (choice) => `${choice.provider.id}:${choice.model}`;
|
|
28
|
+
/** Whether a provider error means the API key is wrong (rather than, say, the server being down). */
|
|
29
|
+
export const rejectedKey = (error) => /\b(401|403)\b|api key|unauthori[sz]ed|authentication/i.test(error);
|
|
30
|
+
/**
|
|
31
|
+
* For an error that says the key was rejected: which key, where it was set, and how to fix it.
|
|
32
|
+
* The usual cause is a project's .env holding a key for the app rather than for Nou.
|
|
33
|
+
*/
|
|
34
|
+
export function keyHint(provider, error) {
|
|
35
|
+
const key = provider.connect?.find((f) => f.secret)?.env;
|
|
36
|
+
if (!key || !rejectedKey(error))
|
|
37
|
+
return null;
|
|
38
|
+
const origin = originOf(key);
|
|
39
|
+
const others = providers.filter((p) => p !== provider && !p.local && !p.unavailable()).map((p) => p.id);
|
|
40
|
+
return `The ${key}${origin ? ` from ${origin}` : ""} was rejected. Enter another with /connect ${provider.id}`
|
|
41
|
+
+ (others.length ? `, or switch with /model ${others.join(" · /model ")}.` : ", or connect another provider (e.g. /connect litellm).");
|
|
42
|
+
}
|
|
27
43
|
/** NOU_MODEL wins, then the model saved in settings, then local Ollama. */
|
|
28
44
|
export function initialModel() {
|
|
29
45
|
if (process.env.NOU_MODEL)
|
|
@@ -2,14 +2,16 @@ import OpenAI from "openai";
|
|
|
2
2
|
import { newCallId } from "./ids.js";
|
|
3
3
|
/** Everything that speaks the OpenAI chat completions API. */
|
|
4
4
|
export const OPENAI_COMPATIBLE = [
|
|
5
|
-
{ id: "openai", label: "OpenAI", baseURL: "https://api.openai.com/v1", keyEnv: "OPENAI_API_KEY", defaultModel: "gpt-5", local: false },
|
|
5
|
+
{ id: "openai", label: "OpenAI", baseURL: "https://api.openai.com/v1", urlEnv: "OPENAI_BASE_URL", keyEnv: "OPENAI_API_KEY", defaultModel: "gpt-5", local: false },
|
|
6
6
|
{ id: "gemini", label: "Google Gemini", baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/", keyEnv: "GEMINI_API_KEY", defaultModel: "gemini-2.5-pro", local: false },
|
|
7
7
|
{ id: "openrouter", label: "OpenRouter", baseURL: "https://openrouter.ai/api/v1", keyEnv: "OPENROUTER_API_KEY", defaultModel: "anthropic/claude-opus-5-5", local: false },
|
|
8
8
|
{ id: "groq", label: "Groq", baseURL: "https://api.groq.com/openai/v1", keyEnv: "GROQ_API_KEY", defaultModel: "llama-3.3-70b-versatile", local: false },
|
|
9
9
|
{ id: "deepseek", label: "DeepSeek", baseURL: "https://api.deepseek.com/v1", keyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek-chat", local: false },
|
|
10
10
|
{ id: "fireworks", label: "Fireworks AI", baseURL: "https://api.fireworks.ai/inference/v1", keyEnv: "FIREWORKS_API_KEY", defaultModel: "accounts/fireworks/models/kimi-k2-instruct", local: false, keyHint: "fireworks.ai/account/api-keys" },
|
|
11
11
|
// A LiteLLM proxy, or any hosted OpenAI-compatible gateway: cloud-sized context, unlike "custom".
|
|
12
|
-
{ id: "litellm", label: "LiteLLM / gateway", baseURL: "", urlEnv: "LITELLM_BASE_URL", keyEnv: "LITELLM_API_KEY", defaultModel: "gpt-5", local: false, keyHint: "your proxy's virtual key or master key"
|
|
12
|
+
{ id: "litellm", label: "LiteLLM / gateway", baseURL: "", urlEnv: "LITELLM_BASE_URL", keyEnv: "LITELLM_API_KEY", defaultModel: "gpt-5", local: false, keyHint: "your proxy's virtual key or master key",
|
|
13
|
+
// The names LiteLLM's own SDK uses for a proxy (its litellm_proxy provider).
|
|
14
|
+
aliases: { url: ["LITELLM_PROXY_API_BASE"], key: ["LITELLM_PROXY_API_KEY"] } },
|
|
13
15
|
{ id: "lmstudio", label: "LM Studio", baseURL: "http://localhost:1234/v1", urlEnv: "LMSTUDIO_BASE_URL", defaultModel: "local-model", local: true },
|
|
14
16
|
{ id: "llamacpp", label: "llama.cpp server", baseURL: "http://localhost:8080/v1", urlEnv: "LLAMACPP_BASE_URL", defaultModel: "local-model", local: true },
|
|
15
17
|
{ id: "custom", label: "Custom OpenAI-compatible", baseURL: "", urlEnv: "NOU_BASE_URL", keyEnv: "NOU_API_KEY", defaultModel: "default", local: true },
|
|
@@ -41,8 +43,9 @@ function toOpenAI(system, messages) {
|
|
|
41
43
|
}
|
|
42
44
|
export function openAICompatibleProvider(preset) {
|
|
43
45
|
// Read when used, not when loaded: keys found in other tools' config (see setup/) are added at startup.
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
+
const first = (names) => names.map((n) => n && process.env[n]).find(Boolean);
|
|
47
|
+
const baseURL = () => first([preset.urlEnv, ...(preset.aliases?.url ?? [])]) || preset.baseURL;
|
|
48
|
+
const apiKey = () => first([preset.keyEnv, ...(preset.aliases?.key ?? [])]) || (preset.local ? "not-needed" : "");
|
|
46
49
|
// Rebuilt when the URL or key changes (they can be entered while Nou runs).
|
|
47
50
|
let client = null;
|
|
48
51
|
const getClient = () => {
|
package/dist/src/syntax/index.js
CHANGED
|
@@ -66,8 +66,17 @@ function parse(file, text) {
|
|
|
66
66
|
load(name);
|
|
67
67
|
if (!lang || typeof lang === "string" || !parser)
|
|
68
68
|
return null;
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
try {
|
|
70
|
+
parser.setLanguage(lang);
|
|
71
|
+
return parser.parse(text);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Some shipped grammars call into the runtime with functions it doesn't have (e.g. YAML's scanner).
|
|
75
|
+
// Don't use that grammar again; callers fall back to highlight.js.
|
|
76
|
+
languages.set(name, "failed");
|
|
77
|
+
parser = new Parser();
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
71
80
|
}
|
|
72
81
|
export function hasGrammar(file) {
|
|
73
82
|
return grammarFor(file) !== undefined;
|