nou-cli 0.5.6 → 0.5.10
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 +12 -6
- package/dist/src/agent/index.js +1 -0
- package/dist/src/cli.js +36 -0
- package/dist/src/config/index.js +19 -0
- package/dist/src/conversations/external.js +35 -13
- package/dist/src/guide/index.js +75 -0
- package/dist/src/ui/App.js +220 -25
- package/dist/src/ui/Chat.js +59 -10
- package/dist/src/ui/CodeView.js +1 -1
- package/dist/src/ui/Editor.js +1 -1
- package/dist/src/ui/GamePanel.js +1 -1
- package/dist/src/ui/Header.js +28 -3
- package/dist/src/ui/Pane.js +2 -2
- package/dist/src/ui/QuickOpen.js +8 -3
- package/dist/src/ui/Settings.js +12 -5
- package/dist/src/ui/Terminal.js +7 -1
- package/dist/src/ui/background.js +58 -0
- package/dist/src/ui/commands.js +45 -6
- package/dist/src/ui/theme.js +59 -13
- package/dist/src/workspaces/client.js +69 -0
- package/dist/src/workspaces/host.js +144 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -50,8 +50,10 @@ Needs **Node 22.12+** and an interactive terminal (Windows Terminal, PowerShell,
|
|
|
50
50
|
| 🔀 **Live diffs** | Every edit Nou proposes shows up side by side, and the files it is editing shimmer in the file tree while it works. <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
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
|
+
| 🪟 **Workspaces** | Several projects in one window, a tab each in the header: click **+** (or `/workspace new ../api`) and the other folder opens beside this one with its own files, chats and agents. Click a tab (or `/workspace 2`) to switch; the ones in the background keep working, and a tab shows ● while its agent is busy. Quitting one (Ctrl+C twice) closes its tab. The first time, Nou installs a small pseudo-terminal package ([@lydell/node-pty](https://www.npmjs.com/package/@lydell/node-pty), prebuilt for macOS, Linux and Windows) and reopens as a workspaces window, with your conversation in the first tab. Or start that way: `nou --workspaces . ../api`. |
|
|
53
54
|
| 🗂️ **Several agents at once** | `/new` (or **+** in the chat's title) opens another conversation tab while the first keeps working. Each tab has its own agent and history; a working tab shows ●, one waiting for your OK shows !, and a click (or `/tab 2`) switches. `/close` closes one; it stays in `/resume`. |
|
|
54
|
-
|
|
|
55
|
+
| 👀 **Show me** | Ask how something works or where it happens (`/showme where do we check the login?`, or just ask) and Nou explains it with the code open: each place it talks about opens in the code view, highlighted, and they're listed in the chat to click. `/showme next` and `/showme back` step through them. Works with every engine: Nou's points at places with a tool, and Claude Code, Codex and pi are asked to list them at the end of their reply, which Nou reads and hides. |
|
|
56
|
+
| 🧑🏫 **Teach me** | `/teach add a --verbose flag` and Nou finds where the change goes, then walks you through typing it yourself: each step opens the file at the right line, says why, and shows the code to type dim ahead of your cursor, like autocomplete (<kbd>Tab</kbd> fills in a line). It moves on when what you typed matches, and saves as it goes. `/teach next` skips a step, `/teach stop` ends it. Works with every engine (Nou's, Claude Code, Codex and pi): any edit one makes anyway is put back. |
|
|
55
57
|
| 🎮 **Games for a break** | Add Pong, Snake, Breakout (it gets faster each wall you clear), Dodge, Hop and Pairs in settings → Games, then `/games` opens a panel docked under the chat: it splits that column instead of covering anything. Drag its title onto the files, code or chat to dock it there, and × at its top right closes it. Drag its top edge to make it taller or shorter. A game runs only while the panel has the keyboard: Esc goes back to your code and it waits. Your best score in each is kept, and a panel too small for a game says how much room it needs. |
|
|
56
58
|
| 🧠 **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 or a connection drops (Codex too: it picks the turn back up instead of stopping at "Reconnecting..."), and conversation caching with Claude. It follows your repo's `AGENTS.md`, `CLAUDE.md` and `NOU.md` (and `~/.nou/NOU.md`). |
|
|
57
59
|
| 🔎 **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). |
|
|
@@ -64,10 +66,12 @@ Needs **Node 22.12+** and an interactive terminal (Windows Terminal, PowerShell,
|
|
|
64
66
|
| 👀 **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. |
|
|
65
67
|
| 🌿 **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`. |
|
|
66
68
|
| 🔌 **Any model** | Local (Ollama, LM Studio, llama.cpp) or cloud (Claude, OpenAI, Gemini, OpenRouter, Groq, DeepSeek). Switch mid-conversation. |
|
|
69
|
+
| 🎚️ **Speed vs smarts** | Pick how hard the model thinks in plain words: **Fastest** and **Fast** (cheaper + faster), **Balanced** (the default), **Smarter** and **Smartest** (thinks longer, costs more). In <kbd>s</kbd> settings, or `/effort smarter`. |
|
|
67
70
|
| 🛞 **Pick your agent** | Nou's own, or Claude Code, Codex or pi, switchable like models, with the same diffs, approvals and undo. |
|
|
68
71
|
| 🧩 **Plugins** | Add your own tools and slash commands with a single file. |
|
|
69
72
|
| 🔎 **Quick open** | <kbd>Ctrl</kbd>+<kbd>P</kbd> fuzzy-finds any file. |
|
|
70
73
|
| 🔋 **Easy on your battery** | Idle, Nou uses next to no CPU. When its window is in the background (in terminals that report focus: iTerm2, Terminal.app, kitty, Ghostty, WezTerm, Windows Terminal, VS Code), spinners and git polling pause until you come back, and TypeScript checks are incremental. |
|
|
74
|
+
| 👓 **Easy to read** | Hints and secondary text keep at least 4.5:1 contrast. Nou asks your terminal whether its background is light or dark and picks matching colors (code switches to GitHub Light on a light background); set it yourself in <kbd>s</kbd> settings → **Nou's colors**. |
|
|
71
75
|
|
|
72
76
|
Every change is reviewed like a code review:
|
|
73
77
|
|
|
@@ -101,14 +105,14 @@ Every change is reviewed like a code review:
|
|
|
101
105
|
| <kbd>c</kbd> | in the file tree: show only changed files |
|
|
102
106
|
| <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) |
|
|
103
107
|
| <kbd><</kbd> / <kbd>></kbd> / <kbd>=</kbd> | make the focused pane narrower / wider / reset the layout |
|
|
104
|
-
| <kbd>`</kbd> | a terminal under the code, like VS Code's (also `/terminal`): Enter runs a command, Ctrl+C stops it, ↑↓ history, Esc back to the code, × closes it; drag its top border (or <kbd>Alt</kbd>+↑/↓) to make it taller or shorter. Commands run through your shell one at a time, so full-screen programs like vim don't draw there |
|
|
108
|
+
| <kbd>`</kbd> | a terminal under the code, like VS Code's (also `/terminal`): Enter runs a command, Ctrl+C stops it, ↑↓ history, Esc back to the code, × or `exit` closes it; drag its top border (or <kbd>Alt</kbd>+↑/↓) to make it taller or shorter. Commands run through your shell one at a time, so full-screen programs like vim don't draw there |
|
|
105
109
|
| <kbd>z</kbd> | in the code view: fold every function and block to its first line, or open them all again (also `/fold`, `/unfold`); click a ▾/▸ beside a line number to fold or open just that one |
|
|
106
110
|
| <kbd>Ctrl</kbd>+<kbd>L</kbd> | draw the whole screen again (also `/redraw`), if it ever looks shifted or clicks land a row off. Output from libraries goes to `~/.nou/nou.log` instead of the screen |
|
|
107
111
|
| <kbd>Ctrl</kbd>+<kbd>O</kbd> | in the chat: open or fold every "▸ N steps" line (what the agent read and ran between replies folds into one; click one to open just it) |
|
|
108
|
-
| <kbd>m</kbd> | mouse on/off, so you can select and copy text in the terminal (in the chat, just drag over the text: it's copied when you let go) |
|
|
112
|
+
| <kbd>m</kbd> | mouse on/off, so you can select and copy text in the terminal (in the chat, just drag over the text: it's copied when you let go; double-click copies a word, path or link, triple-click a paragraph) |
|
|
109
113
|
| <kbd>s</kbd> | settings: model, preferences, plugins |
|
|
110
114
|
| <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) |
|
|
111
|
-
| <kbd>Enter</kbd> while Nou works | queue the message: it goes when Nou finishes, together with anything else you queued, as one message; commands that only change the screen, like `/terminal`, `/games` or `/open`, run right away
|
|
115
|
+
| <kbd>Enter</kbd> while Nou works | queue the message: it goes when Nou finishes, together with anything else you queued, as one message; commands that only change the screen, like `/terminal`, `/games` or `/open`, run right away. To send now instead of waiting: <kbd>Alt</kbd>+<kbd>Enter</kbd> (stops Nou and sends it), click the queued message, or `/now`; or turn on **Send while Nou works** in settings so <kbd>Enter</kbd> sends right away (<kbd>Alt</kbd>+<kbd>Enter</kbd> then queues) |
|
|
112
116
|
| <kbd>Esc</kbd> | stop Nou (anything queued goes back in the input) · close a diff · back to chat |
|
|
113
117
|
| <kbd>Ctrl</kbd>+<kbd>C</kbd> | stop Nou while it works · press twice to quit |
|
|
114
118
|
|
|
@@ -126,11 +130,13 @@ In the editor:
|
|
|
126
130
|
| <kbd>Ctrl</kbd>+<kbd>K</kbd> · <kbd>Ctrl</kbd>+<kbd>G</kbd> | hover info · go to definition (with a language server) |
|
|
127
131
|
| <kbd>Esc</kbd> | clear the selection, or leave the editor |
|
|
128
132
|
|
|
129
|
-
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` · `/resume [n|last]` · `/terminal` · `/showme <
|
|
133
|
+
Chat commands: `/settings` · `/model [provider:model]` · `/models [provider]` · `/pull <model>` · `/engine [name]` · `/effort [fastest|fast|balanced|smarter|smartest]` · `/auto [ask|auto-edit|auto]` · `/copy [code|file]` · `/undo` · `/rewind` · `/history` · `/diff [file or n]` · `/sessions` · `/problems` · `/compact` · `/clear` · `/resume [n|last]` · `/terminal` · `/workspace [new <folder> | n]` · `/now` · `/showme <question>` · `/teach <change>` · `/help`
|
|
134
|
+
|
|
135
|
+
A small typo in a command still works when it only changes the screen (`/termnial` opens the terminal); for others Nou asks "Did you mean...?" instead of guessing.
|
|
130
136
|
|
|
131
137
|
New lines in a message: <kbd>Shift</kbd>/<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Enter</kbd> in terminals that report them (kitty, Ghostty, WezTerm, iTerm2 and others with the kitty keyboard protocol), or type `\` and press <kbd>Enter</kbd> in any terminal.
|
|
132
138
|
|
|
133
|
-
Conversations are saved per folder after each reply. `/resume`
|
|
139
|
+
Conversations are saved per folder after each reply. `/resume` opens a picker (↑↓ and Enter, type to filter, or type its number) and `/resume 2` (or `nou --continue` for the last one, `nou --resume` for the list) picks one back up with the engine it was on: Nou's own engine restores its messages, Claude Code its session and Codex its thread; pi is handed the conversation so far. Sessions you started in Claude Code's or Codex's own CLI in this folder are listed too (marked "CLI"), and resuming one carries it on in that engine inside Nou.
|
|
134
140
|
|
|
135
141
|
## 🧠 Models
|
|
136
142
|
|
package/dist/src/agent/index.js
CHANGED
|
@@ -49,6 +49,7 @@ How to work:
|
|
|
49
49
|
- Don't commit, push, install packages or delete files unless the user asked. Don't add comments the code didn't need.
|
|
50
50
|
- Before saying something works, check it (run the tests or the code). If you couldn't, say so.
|
|
51
51
|
- 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.
|
|
52
|
+
- When you explain how something works and it happens in more than one place, call show_places with each part (file, line range, a short note) in the order to read them: Nou opens them highlighted, one after another. Then explain in that order.
|
|
52
53
|
- If the user rejects an edit or command, don't retry it: ask what they would like instead.
|
|
53
54
|
- Keep replies short. Refer to code as path:line.
|
|
54
55
|
|
package/dist/src/cli.js
CHANGED
|
@@ -15,6 +15,11 @@ import { App } from "./ui/App.js";
|
|
|
15
15
|
import { installFastOutput } from "./ui/fast-output.js";
|
|
16
16
|
import { RENDER_OPTIONS } from "./ui/render-options.js";
|
|
17
17
|
import { setScreenClear } from "./ui/redraw.js";
|
|
18
|
+
import { detectBackground } from "./ui/background.js";
|
|
19
|
+
import { setPalette } from "./ui/theme.js";
|
|
20
|
+
import { runHost, workspaceFolder } from "./workspaces/host.js";
|
|
21
|
+
import { pendingWorkspaces } from "./workspaces/client.js";
|
|
22
|
+
import { spawn } from "child_process";
|
|
18
23
|
import { appendFileSync, mkdirSync } from "fs";
|
|
19
24
|
import path from "path";
|
|
20
25
|
import { format } from "util";
|
|
@@ -47,6 +52,7 @@ const HELP = `nou ${currentVersion()}: an AI coding assistant for your terminal
|
|
|
47
52
|
Usage:
|
|
48
53
|
nou open Nou in the current folder
|
|
49
54
|
nou --continue pick up the last conversation in this folder (-c)
|
|
55
|
+
nou --workspaces . ../api several projects in one window, a tab each
|
|
50
56
|
nou --resume list earlier conversations here to pick one up (-r)
|
|
51
57
|
nou update update to the latest version
|
|
52
58
|
nou --version print the version
|
|
@@ -76,6 +82,24 @@ async function main() {
|
|
|
76
82
|
console.log("If that was a permissions error, run: sudo npm install -g nou-cli@latest");
|
|
77
83
|
process.exit(1);
|
|
78
84
|
}
|
|
85
|
+
// nou --workspaces <folder>... : several Nous in this window, a tab each (see src/workspaces).
|
|
86
|
+
const wsAt = args.indexOf("--workspaces");
|
|
87
|
+
if (wsAt >= 0) {
|
|
88
|
+
if (!process.stdin.isTTY) {
|
|
89
|
+
console.error("Nou needs an interactive terminal.");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const asked = args.slice(wsAt + 1).filter((a) => !a.startsWith("-"));
|
|
93
|
+
const folders = asked.map((f) => workspaceFolder(f, process.cwd())).filter((f) => !!f);
|
|
94
|
+
const missing = asked.filter((f) => !workspaceFolder(f, process.cwd()));
|
|
95
|
+
if (missing.length)
|
|
96
|
+
console.error(`Not a folder, so not opened: ${missing.join(", ")}`);
|
|
97
|
+
const uiTheme = loadSettings().uiTheme;
|
|
98
|
+
// Every workspace uses the colors found for this terminal (they can't ask it themselves from the background).
|
|
99
|
+
process.env.NOU_PALETTE = uiTheme === "dark" || uiTheme === "light" ? uiTheme : (await detectBackground()) ?? "dark";
|
|
100
|
+
const firstArgs = args.includes("--continue") || args.includes("-c") ? ["--continue"] : [];
|
|
101
|
+
process.exit(await runHost(folders.length ? folders : [process.cwd()], firstArgs));
|
|
102
|
+
}
|
|
79
103
|
if (!process.stdin.isTTY) {
|
|
80
104
|
console.error("Nou needs an interactive terminal (try Windows Terminal, PowerShell, or Terminal/iTerm on macOS).");
|
|
81
105
|
process.exit(1);
|
|
@@ -136,6 +160,10 @@ async function main() {
|
|
|
136
160
|
if ((settings.engine ?? "nou") === "nou")
|
|
137
161
|
await ensureProjectContext(choice);
|
|
138
162
|
const permContext = await loadPermanentContext();
|
|
163
|
+
// Colors that are readable on this terminal: a dark or a light set, from the setting or the background.
|
|
164
|
+
const uiTheme = loadSettings().uiTheme;
|
|
165
|
+
const fromHost = process.env.NOU_PALETTE;
|
|
166
|
+
setPalette(uiTheme === "dark" || uiTheme === "light" ? uiTheme : fromHost === "light" || fromHost === "dark" ? fromHost : (await detectBackground()) ?? "dark");
|
|
139
167
|
await installFastOutput();
|
|
140
168
|
// While Nou is on screen, anything a library prints (console.log, warnings, stderr) goes to a log file:
|
|
141
169
|
// written into the terminal, it would scroll the screen out from under the mouse.
|
|
@@ -144,6 +172,14 @@ async function main() {
|
|
|
144
172
|
setScreenClear(() => app.clear());
|
|
145
173
|
await app.waitUntilExit();
|
|
146
174
|
restoreOutput();
|
|
175
|
+
// /workspace new: Nou closes and opens again as a workspaces window, this conversation in the first tab.
|
|
176
|
+
const folders = pendingWorkspaces();
|
|
177
|
+
if (folders) {
|
|
178
|
+
lsp.stopAll();
|
|
179
|
+
const child = spawn(process.execPath, [process.argv[1], "--workspaces", ...folders, "--continue"], { stdio: "inherit" });
|
|
180
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
147
183
|
lsp.stopAll();
|
|
148
184
|
p.outro(chalk.cyan("Bye bye!"));
|
|
149
185
|
}
|
package/dist/src/config/index.js
CHANGED
|
@@ -4,6 +4,23 @@ import path from "path";
|
|
|
4
4
|
export const NOU_HOME = path.join(os.homedir(), ".nou");
|
|
5
5
|
const CONFIG_PATH = path.join(NOU_HOME, "config.json");
|
|
6
6
|
export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
7
|
+
/**
|
|
8
|
+
* What each effort means for you, since "xhigh" says little: a short name for the picker and what it trades.
|
|
9
|
+
* The saved value stays the effort itself (low...max), which is what the engines are given.
|
|
10
|
+
*/
|
|
11
|
+
export const EFFORT_LABELS = {
|
|
12
|
+
low: { name: "Fastest", tradeoff: "Cheapest + fastest: quick answers, little thinking. Good for small edits and questions." },
|
|
13
|
+
medium: { name: "Fast", tradeoff: "Cheaper + faster: some thinking, for everyday changes." },
|
|
14
|
+
high: { name: "Balanced", tradeoff: "Balanced: thinks things through without taking long. The default." },
|
|
15
|
+
xhigh: { name: "Smarter", tradeoff: "Smarter: thinks longer, for trickier bugs and bigger changes. Slower, costs more." },
|
|
16
|
+
max: { name: "Smartest", tradeoff: "Smartest: thinks as long as it needs, for the hardest problems. Slowest, costs the most." },
|
|
17
|
+
};
|
|
18
|
+
/** An effort from its name or its label ("smarter", "xhigh"), any case. */
|
|
19
|
+
export function effortFrom(text) {
|
|
20
|
+
const t = text.trim().toLowerCase();
|
|
21
|
+
return EFFORTS.find((e) => e === t || EFFORT_LABELS[e].name.toLowerCase() === t) ?? null;
|
|
22
|
+
}
|
|
23
|
+
export const UI_THEMES = ["auto", "dark", "light"];
|
|
7
24
|
export const AUTO_SAVE = ["delay", "focus", "off"];
|
|
8
25
|
/** How fast follow mode replays what the agent does: a step every so often, or as it happens. */
|
|
9
26
|
export const FOLLOW_SPEEDS = ["slow", "normal", "fast", "instant"];
|
|
@@ -18,8 +35,10 @@ export const DEFAULT_SETTINGS = {
|
|
|
18
35
|
autoApproveEdits: false,
|
|
19
36
|
autoApproveCommands: false,
|
|
20
37
|
gitContext: true,
|
|
38
|
+
sendNowWhileBusy: false,
|
|
21
39
|
autoSave: "delay",
|
|
22
40
|
codeTheme: "one-dark",
|
|
41
|
+
uiTheme: "auto",
|
|
23
42
|
mouse: true,
|
|
24
43
|
reuseToolConfig: true,
|
|
25
44
|
paneWidths: null,
|
|
@@ -29,11 +29,27 @@ function jsonLines(file, limit = Infinity) {
|
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
31
31
|
/** The start of a file, for the line that says which folder a Codex session ran in. */
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The file's first line, however long (up to `max`): Codex puts its whole system prompt in a session's first
|
|
34
|
+
* line, tens of KB, and reading a fixed 8 KB cut it short, so no Codex session was ever found.
|
|
35
|
+
*/
|
|
36
|
+
function firstLine(file, max = 4 * 1024 * 1024) {
|
|
33
37
|
const fd = openSync(file, "r");
|
|
34
38
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let read = 0;
|
|
41
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
42
|
+
while (read < max) {
|
|
43
|
+
const n = readSync(fd, buf, 0, buf.length, read);
|
|
44
|
+
if (n <= 0)
|
|
45
|
+
break;
|
|
46
|
+
const end = buf.subarray(0, n).indexOf(10); // \n
|
|
47
|
+
chunks.push(Buffer.from(buf.subarray(0, end >= 0 ? end : n)));
|
|
48
|
+
read += n;
|
|
49
|
+
if (end >= 0)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
37
53
|
}
|
|
38
54
|
finally {
|
|
39
55
|
closeSync(fd);
|
|
@@ -90,28 +106,34 @@ function codexSessions(home, cwd) {
|
|
|
90
106
|
if (!existsSync(root))
|
|
91
107
|
return [];
|
|
92
108
|
const files = [];
|
|
109
|
+
// By date: sessions/YYYY/MM/DD/rollout-*.jsonl. Anything else in there (a .DS_Store, a folder that can't
|
|
110
|
+
// be read) is skipped, rather than hiding every session.
|
|
93
111
|
const walk = (dir, depth) => {
|
|
94
|
-
|
|
112
|
+
let names;
|
|
113
|
+
try {
|
|
114
|
+
names = readdirSync(dir).sort().reverse();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const name of names) {
|
|
95
120
|
if (files.length >= SCAN)
|
|
96
121
|
return;
|
|
97
122
|
const full = path.join(dir, name);
|
|
98
|
-
if (depth < 3)
|
|
99
|
-
|
|
123
|
+
if (depth < 3) {
|
|
124
|
+
if (!name.startsWith("."))
|
|
125
|
+
walk(full, depth + 1);
|
|
126
|
+
}
|
|
100
127
|
else if (name.startsWith("rollout-") && name.endsWith(".jsonl"))
|
|
101
128
|
files.push(full);
|
|
102
129
|
}
|
|
103
130
|
};
|
|
104
|
-
|
|
105
|
-
walk(root, 0);
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
131
|
+
walk(root, 0);
|
|
110
132
|
const out = [];
|
|
111
133
|
for (const file of files) {
|
|
112
134
|
let meta;
|
|
113
135
|
try {
|
|
114
|
-
meta = JSON.parse(
|
|
136
|
+
meta = JSON.parse(firstLine(file));
|
|
115
137
|
}
|
|
116
138
|
catch {
|
|
117
139
|
continue;
|
package/dist/src/guide/index.js
CHANGED
|
@@ -98,3 +98,78 @@ export function guideProgress(typed, expected) {
|
|
|
98
98
|
}
|
|
99
99
|
return j;
|
|
100
100
|
}
|
|
101
|
+
export const SHOW_PLACES = "show_places";
|
|
102
|
+
/** Stops as the model gave them, kept when usable: a file in the project and a line. */
|
|
103
|
+
export function tidyStops(raw) {
|
|
104
|
+
return (Array.isArray(raw) ? raw : [])
|
|
105
|
+
.filter((s) => typeof s?.file === "string" && s.file.trim() && Number(s.line) >= 1)
|
|
106
|
+
.map((s) => {
|
|
107
|
+
const line = Math.floor(Number(s.line));
|
|
108
|
+
const end = Math.max(line, Math.floor(Number(s.end_line ?? s.end ?? line)) || line);
|
|
109
|
+
return { file: path.relative(process.cwd(), path.resolve(s.file)).split(path.sep).join("/"), line, end, note: String(s.note ?? s.why ?? "").trim() };
|
|
110
|
+
})
|
|
111
|
+
.filter((s) => !s.file.startsWith(".."))
|
|
112
|
+
.slice(0, 12);
|
|
113
|
+
}
|
|
114
|
+
/** The tool Nou's own engine points at places with. `onStops` gets them. */
|
|
115
|
+
export function showPlacesTool(onStops) {
|
|
116
|
+
return {
|
|
117
|
+
definition: {
|
|
118
|
+
name: SHOW_PLACES,
|
|
119
|
+
description: "When you explain how something works or where something happens in this project, point at the exact places so the user sees them: Nou opens each one in their code view, highlighted, in the order given. Call it once, after you've read the code, with the parts that matter (usually 1-6).",
|
|
120
|
+
parameters: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
places: {
|
|
124
|
+
type: "array",
|
|
125
|
+
items: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
file: { type: "string", description: "Project-relative path." },
|
|
129
|
+
line: { type: "number", description: "First line of the part (1-based)." },
|
|
130
|
+
end_line: { type: "number", description: "Last line of the part." },
|
|
131
|
+
note: { type: "string", description: "One or two plain sentences: what this part does, in terms of the explanation." },
|
|
132
|
+
},
|
|
133
|
+
required: ["file", "line", "end_line", "note"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
required: ["places"],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
readOnly: true,
|
|
141
|
+
source: "nou",
|
|
142
|
+
run: (args) => {
|
|
143
|
+
const stops = tidyStops(args.places);
|
|
144
|
+
if (!stops.length)
|
|
145
|
+
return "No usable places: each needs a project file and a line. Call show_places again.";
|
|
146
|
+
onStops(stops);
|
|
147
|
+
return `Shown to the user: ${stops.length} place${stops.length > 1 ? "s" : ""}, opening the first. Now explain in the order of the places, naming each as path:line.`;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** The fenced block other engines (which can't call Nou's tools) list their places in. */
|
|
152
|
+
const SHOW_BLOCK = /```nou-show\s*\n([\s\S]*?)\n```/;
|
|
153
|
+
/**
|
|
154
|
+
* For Claude Code, Codex and pi, which can't call show_places: how to point at places, added to what they
|
|
155
|
+
* know about the project, so explaining "where does X happen" opens the files on any engine.
|
|
156
|
+
*/
|
|
157
|
+
export const SHOW_PLACES_NOTE = "When you explain how something in this project works or where something happens, end your reply with the exact places, in the order to read them, as a JSON array in a fenced block marked nou-show. The user's editor opens each one highlighted:\n```nou-show\n[{\"file\": \"src/auth/session.ts\", \"line\": 40, \"end_line\": 58, \"note\": \"Checks the token and refuses expired ones.\"}]\n```\nOnly for places in this project you've actually read; leave it out when nothing needs showing.";
|
|
158
|
+
/** The message /showme sends: explain, and point at the places. */
|
|
159
|
+
export function explainPrompt(question, withTool) {
|
|
160
|
+
const how = withTool ? `call ${SHOW_PLACES} with them` : "list them in a nou-show block at the end, as described";
|
|
161
|
+
return `[Show me] ${question}\n\nExplain this to me and show me where it happens in the code. Don't change any files. Read what you need, find the exact places (file and line range) that matter, ${how}, in the order to read them, and explain in that order, naming each as path:line.`;
|
|
162
|
+
}
|
|
163
|
+
/** Places from a reply's ```nou-show block, and the reply without it; null when there's none. */
|
|
164
|
+
export function stopsFromReply(text) {
|
|
165
|
+
const m = SHOW_BLOCK.exec(text);
|
|
166
|
+
if (!m)
|
|
167
|
+
return null;
|
|
168
|
+
try {
|
|
169
|
+
const stops = tidyStops(JSON.parse(m[1]));
|
|
170
|
+
return { stops, rest: text.replace(m[0], "").trim() };
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|