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/dist/src/ui/text.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import { highlight, supportsLanguage } from "cli-highlight";
|
|
3
2
|
import { diffLines } from "diff";
|
|
4
|
-
import { highlightTree } from "../syntax/index.js";
|
|
3
|
+
import { highlightTree, notifySyntaxLoaded } from "../syntax/index.js";
|
|
4
|
+
import { highlightJsTheme } from "../syntax/themes.js";
|
|
5
|
+
let fallback = null;
|
|
6
|
+
let loading = null;
|
|
7
|
+
export function loadFallbackHighlighter() {
|
|
8
|
+
loading ??= import("cli-highlight").then((m) => {
|
|
9
|
+
fallback = { highlight: m.highlight, supportsLanguage: m.supportsLanguage };
|
|
10
|
+
notifySyntaxLoaded();
|
|
11
|
+
}, () => { });
|
|
12
|
+
return loading;
|
|
13
|
+
}
|
|
5
14
|
const LANGUAGES = {
|
|
6
15
|
".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
7
16
|
".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
|
@@ -23,10 +32,16 @@ export function highlightLines(filePath, text) {
|
|
|
23
32
|
if (tree)
|
|
24
33
|
return tree;
|
|
25
34
|
const language = LANGUAGES[path.extname(filePath).toLowerCase()];
|
|
26
|
-
if (!language ||
|
|
35
|
+
if (!language || text.length > 200_000)
|
|
36
|
+
return plain;
|
|
37
|
+
if (!fallback) {
|
|
38
|
+
void loadFallbackHighlighter();
|
|
39
|
+
return plain;
|
|
40
|
+
}
|
|
41
|
+
if (!fallback.supportsLanguage(language))
|
|
27
42
|
return plain;
|
|
28
43
|
try {
|
|
29
|
-
return highlight(plain.join("\n"), { language, ignoreIllegals: true }).split("\n");
|
|
44
|
+
return fallback.highlight(plain.join("\n"), { language, ignoreIllegals: true, theme: highlightJsTheme() }).split("\n");
|
|
30
45
|
}
|
|
31
46
|
catch {
|
|
32
47
|
return plain;
|
|
@@ -135,7 +150,7 @@ export function inlineSpans(text, base = { text: "" }) {
|
|
|
135
150
|
out.push({ ...base, text: text.slice(pos) });
|
|
136
151
|
return out.filter((s) => s.text);
|
|
137
152
|
}
|
|
138
|
-
const sameStyle = (a, b) => !!a.bold === !!b.bold && !!a.italic === !!b.italic && !!a.code === !!b.code && !!a.dim === !!b.dim;
|
|
153
|
+
const sameStyle = (a, b) => !!a.bold === !!b.bold && !!a.italic === !!b.italic && !!a.code === !!b.code && !!a.dim === !!b.dim && a.link === b.link;
|
|
139
154
|
/** Word-wraps styled spans; continuation lines start with `indent` spaces. */
|
|
140
155
|
function wrapSpans(spans, width, indent = 0) {
|
|
141
156
|
const max = Math.max(1, width);
|
|
@@ -186,7 +201,9 @@ function wrapSpans(spans, width, indent = 0) {
|
|
|
186
201
|
}
|
|
187
202
|
const cells = (row) => row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
188
203
|
/** Renders markdown for the terminal: headings, lists, quotes, code blocks, tables and inline styles, wrapped to `width`. */
|
|
189
|
-
export function renderMarkdown(text, width) {
|
|
204
|
+
export function renderMarkdown(text, width, isFile) {
|
|
205
|
+
// File references become links before wrapping, so a long path that wraps is still one link.
|
|
206
|
+
const spansOf = (line, base) => (isFile ? linkFileRefs(inlineSpans(line, base), isFile) : inlineSpans(line, base));
|
|
190
207
|
const out = [];
|
|
191
208
|
const src = splitLines(text);
|
|
192
209
|
let fence = null;
|
|
@@ -215,7 +232,7 @@ export function renderMarkdown(text, width) {
|
|
|
215
232
|
for (; j < src.length && /^\s*\|/.test(src[j]); j++)
|
|
216
233
|
rows.push(cells(src[j]));
|
|
217
234
|
i = j - 1;
|
|
218
|
-
const plain = rows.map((r) => r.map((c) =>
|
|
235
|
+
const plain = rows.map((r) => r.map((c) => spansOf(c).map((s) => s.text).join("")));
|
|
219
236
|
const columns = Math.max(...plain.map((r) => r.length));
|
|
220
237
|
const widths = Array.from({ length: columns }, (_, c) => Math.max(...plain.map((r) => (r[c] ?? "").length)));
|
|
221
238
|
const fits = widths.reduce((a, b) => a + b, 0) + 3 * (columns - 1) <= width;
|
|
@@ -224,7 +241,7 @@ export function renderMarkdown(text, width) {
|
|
|
224
241
|
row.forEach((cell, c) => {
|
|
225
242
|
if (c > 0)
|
|
226
243
|
spans.push({ text: " │ ", dim: true });
|
|
227
|
-
spans.push(...
|
|
244
|
+
spans.push(...spansOf(cell, r === 0 ? { text: "", bold: true } : { text: "" }));
|
|
228
245
|
if (fits && c < columns - 1)
|
|
229
246
|
spans.push({ text: " ".repeat(widths[c] - plain[r][c].length) });
|
|
230
247
|
});
|
|
@@ -237,7 +254,7 @@ export function renderMarkdown(text, width) {
|
|
|
237
254
|
}
|
|
238
255
|
const heading = raw.match(/^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/);
|
|
239
256
|
if (heading) {
|
|
240
|
-
for (const line of wrapSpans(
|
|
257
|
+
for (const line of wrapSpans(spansOf(heading[2], { text: "", bold: true }), width))
|
|
241
258
|
out.push({ kind: "heading", spans: line });
|
|
242
259
|
continue;
|
|
243
260
|
}
|
|
@@ -247,7 +264,7 @@ export function renderMarkdown(text, width) {
|
|
|
247
264
|
}
|
|
248
265
|
const quote = raw.match(/^\s*>\s?(.*)/);
|
|
249
266
|
if (quote) {
|
|
250
|
-
for (const line of wrapSpans(
|
|
267
|
+
for (const line of wrapSpans(spansOf(quote[1], { text: "", italic: true }), width - 2))
|
|
251
268
|
out.push({ kind: "quote", spans: [{ text: "│ ", dim: true }, ...line] });
|
|
252
269
|
continue;
|
|
253
270
|
}
|
|
@@ -256,12 +273,99 @@ export function renderMarkdown(text, width) {
|
|
|
256
273
|
const depth = Math.floor(item[1].length / 2) * 2;
|
|
257
274
|
const task = item[3] ? (/x/i.test(item[3]) ? "☑ " : "☐ ") : "";
|
|
258
275
|
const bullet = `${" ".repeat(depth)}${/\d/.test(item[2]) ? item[2] : "•"} ${task}`;
|
|
259
|
-
for (const line of wrapSpans([{ text: bullet }, ...
|
|
276
|
+
for (const line of wrapSpans([{ text: bullet }, ...spansOf(item[4])], width, bullet.length))
|
|
260
277
|
out.push({ kind: "text", spans: line });
|
|
261
278
|
continue;
|
|
262
279
|
}
|
|
263
|
-
for (const line of wrapSpans(
|
|
280
|
+
for (const line of wrapSpans(spansOf(raw), width))
|
|
264
281
|
out.push({ kind: "text", spans: line });
|
|
265
282
|
}
|
|
266
283
|
return out;
|
|
267
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* path, path:line or path:line-line; a path has a dot in its last part, so plain words don't match.
|
|
287
|
+
* Windows paths (C:\\dir\\file.ts, dir\\file.ts) count too.
|
|
288
|
+
*/
|
|
289
|
+
const FILE_REF = /(?<![\w/\\.@~-])((?:[A-Za-z]:[\\/]|\.{0,2}[\\/])?(?:[\w@.~+-]+[\\/])*[\w@-][\w@.~+-]*\.[A-Za-z0-9]{1,8})(?::(\d+)(?:[-–](\d+))?)?(?![\w/\\])/g;
|
|
290
|
+
/** Splits out the references to files that exist (per `isFile`) as link spans, keeping each span's style. */
|
|
291
|
+
export function linkFileRefs(spans, isFile) {
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const span of spans) {
|
|
294
|
+
let pos = 0;
|
|
295
|
+
for (const m of span.text.matchAll(FILE_REF)) {
|
|
296
|
+
const file = m[1].replace(/^\.[\\/]/, "");
|
|
297
|
+
if (!isFile(file))
|
|
298
|
+
continue;
|
|
299
|
+
if (m.index > pos)
|
|
300
|
+
out.push({ ...span, text: span.text.slice(pos, m.index) });
|
|
301
|
+
const link = { path: file };
|
|
302
|
+
if (m[2])
|
|
303
|
+
link.line = Number(m[2]);
|
|
304
|
+
if (m[3])
|
|
305
|
+
link.to = Number(m[3]);
|
|
306
|
+
out.push({ ...span, text: m[0], link });
|
|
307
|
+
pos = m.index + m[0].length;
|
|
308
|
+
}
|
|
309
|
+
if (pos < span.text.length)
|
|
310
|
+
out.push(pos ? { ...span, text: span.text.slice(pos) } : span);
|
|
311
|
+
}
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
/** The link at column `col` of a line of spans, if any. */
|
|
315
|
+
export function linkAt(spans, col) {
|
|
316
|
+
let x = 0;
|
|
317
|
+
for (const span of spans) {
|
|
318
|
+
if (col >= x && col < x + span.text.length)
|
|
319
|
+
return span.link ?? null;
|
|
320
|
+
x += span.text.length;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
// eslint-disable-next-line no-control-regex -- color codes start with ESC
|
|
325
|
+
const ESCAPE = /\x1b\[[\d;]*m/y;
|
|
326
|
+
/**
|
|
327
|
+
* Columns `from` to `from + width` of a highlighted line, keeping its colors: the color codes before
|
|
328
|
+
* `from` are kept (so the text still starts in the right color), the characters are dropped.
|
|
329
|
+
*/
|
|
330
|
+
export function sliceAnsi(line, from, width = Infinity) {
|
|
331
|
+
let out = "";
|
|
332
|
+
let column = 0;
|
|
333
|
+
for (let i = 0; i < line.length;) {
|
|
334
|
+
ESCAPE.lastIndex = i;
|
|
335
|
+
const code = ESCAPE.exec(line);
|
|
336
|
+
if (code) {
|
|
337
|
+
out += code[0];
|
|
338
|
+
i += code[0].length;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const ch = String.fromCodePoint(line.codePointAt(i));
|
|
342
|
+
if (column >= from && column < from + width)
|
|
343
|
+
out += ch;
|
|
344
|
+
column++;
|
|
345
|
+
i += ch.length;
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
/** Wraps visible columns `from`..`to` of a highlighted line in `open`/`close` codes (e.g. to mark a search match). */
|
|
350
|
+
export function markAnsi(line, from, to, open = "\x1b[7m", close = "\x1b[27m") {
|
|
351
|
+
let out = "";
|
|
352
|
+
let column = 0;
|
|
353
|
+
for (let i = 0; i < line.length;) {
|
|
354
|
+
ESCAPE.lastIndex = i;
|
|
355
|
+
const code = ESCAPE.exec(line);
|
|
356
|
+
if (code) {
|
|
357
|
+
out += code[0];
|
|
358
|
+
i += code[0].length;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const ch = String.fromCodePoint(line.codePointAt(i));
|
|
362
|
+
if (column === from)
|
|
363
|
+
out += open;
|
|
364
|
+
out += ch;
|
|
365
|
+
column++;
|
|
366
|
+
if (column === to)
|
|
367
|
+
out += close;
|
|
368
|
+
i += ch.length;
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Stands in for string-width (see fast-width.ts): the real one, named in this module's URL, made faster. */
|
|
2
|
+
import { fastStringWidth } from "./fast-width.js";
|
|
3
|
+
const of = new URL(import.meta.url).searchParams.get("of");
|
|
4
|
+
if (!of)
|
|
5
|
+
throw new Error("width-shim needs ?of=<string-width url>");
|
|
6
|
+
const real = (await import(of)).default;
|
|
7
|
+
export default fastStringWidth(real);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nou-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Nou: an IDE-style AI coding assistant for your terminal. Local (Ollama, LM Studio) and cloud (Claude, OpenAI, Gemini...) models, live diffs, git-aware, built-in editor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@anthropic-ai/sdk": "^0.133.0",
|
|
49
49
|
"@clack/prompts": "^1.1.0",
|
|
50
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
50
51
|
"chalk": "^5.6.2",
|
|
51
52
|
"cli-highlight": "^2.1.11",
|
|
52
53
|
"diff": "^9.0.0",
|