nou-cli 0.2.0 → 0.3.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.
Files changed (41) hide show
  1. package/.env.example +12 -0
  2. package/README.md +42 -6
  3. package/dist/index.js +22 -2
  4. package/dist/src/agent/harness.js +54 -0
  5. package/dist/src/agent/index.js +159 -15
  6. package/dist/src/commands/index.js +54 -5
  7. package/dist/src/config/credentials.js +48 -0
  8. package/dist/src/config/index.js +3 -0
  9. package/dist/src/config/origins.js +14 -0
  10. package/dist/src/context/index.js +1 -1
  11. package/dist/src/engines/claude.js +147 -0
  12. package/dist/src/engines/codex.js +131 -0
  13. package/dist/src/engines/external.js +152 -0
  14. package/dist/src/engines/index.js +127 -0
  15. package/dist/src/engines/pi.js +161 -0
  16. package/dist/src/engines/types.js +1 -0
  17. package/dist/src/env.js +5 -1
  18. package/dist/src/git/index.js +14 -4
  19. package/dist/src/history/index.js +53 -1
  20. package/dist/src/plugins/index.js +109 -21
  21. package/dist/src/providers/anthropic.js +16 -1
  22. package/dist/src/providers/ollama.js +58 -1
  23. package/dist/src/providers/openai.js +22 -5
  24. package/dist/src/search/index.js +183 -0
  25. package/dist/src/setup/index.js +240 -0
  26. package/dist/src/tools/index.js +72 -2
  27. package/dist/src/ui/App.js +351 -72
  28. package/dist/src/ui/Chat.js +9 -3
  29. package/dist/src/ui/CodeView.js +5 -1
  30. package/dist/src/ui/Editor.js +5 -1
  31. package/dist/src/ui/FileTree.js +18 -1
  32. package/dist/src/ui/Header.js +3 -1
  33. package/dist/src/ui/ProblemsView.js +5 -1
  34. package/dist/src/ui/Settings.js +93 -14
  35. package/dist/src/ui/mouse.js +52 -0
  36. package/dist/src/ui/text.js +12 -0
  37. package/package.json +5 -3
  38. package/plugins/fetch-url.mjs +82 -0
  39. package/plugins/project-rules.mjs +43 -0
  40. package/plugins/run-tests.mjs +89 -0
  41. package/plugins/todos.mjs +81 -0
@@ -0,0 +1,89 @@
1
+ // Starter plugin: lets the model (and you, with /test) run the project's tests.
2
+ // It's yours to edit: set TEST_COMMAND if the guess below picks the wrong command.
3
+ import { spawn } from "child_process";
4
+ import { existsSync, readFileSync } from "fs";
5
+ import path from "path";
6
+ import process from "process";
7
+
8
+ /** e.g. "npm run test:unit". Leave null to guess from the project files. */
9
+ const TEST_COMMAND = null;
10
+ const TIMEOUT_MS = 5 * 60 * 1000;
11
+ const MAX_OUTPUT = 12_000;
12
+
13
+ function guessCommand(cwd) {
14
+ const has = (file) => existsSync(path.join(cwd, file));
15
+ if (has("package.json")) {
16
+ const test = JSON.parse(readFileSync(path.join(cwd, "package.json"), "utf-8")).scripts?.test;
17
+ if (test && !test.includes("no test specified")) {
18
+ if (has("pnpm-lock.yaml")) return "pnpm test";
19
+ if (has("yarn.lock")) return "yarn test";
20
+ if (has("bun.lock") || has("bun.lockb")) return "bun test";
21
+ return "npm test";
22
+ }
23
+ }
24
+ if (has("Cargo.toml")) return "cargo test";
25
+ if (has("go.mod")) return "go test ./...";
26
+ if (has("pyproject.toml") || has("pytest.ini") || has("setup.py") || has("tox.ini")) return "python -m pytest -q";
27
+ if (has("Makefile") && /^test:/m.test(readFileSync(path.join(cwd, "Makefile"), "utf-8"))) return "make test";
28
+ return null;
29
+ }
30
+
31
+ /** Runs the tests; extra arguments (a file, a filter) go after the command. */
32
+ function runTests(cwd, extra) {
33
+ const base = TEST_COMMAND ?? guessCommand(cwd);
34
+ if (!base) return Promise.resolve({ command: null, code: null, output: "Couldn't tell how to run this project's tests. Set TEST_COMMAND in the run-tests plugin." });
35
+ const separator = extra && /^(npm|pnpm) /.test(base) ? " --" : "";
36
+ const command = extra ? `${base}${separator} ${extra}` : base;
37
+ return new Promise((resolve) => {
38
+ let output = "";
39
+ const child = spawn(command, { cwd, shell: true, env: { ...process.env, CI: "1", FORCE_COLOR: "0" } });
40
+ const timer = setTimeout(() => child.kill(), TIMEOUT_MS);
41
+ child.stdout.on("data", (d) => (output += d));
42
+ child.stderr.on("data", (d) => (output += d));
43
+ child.on("error", (error) => (output += `\n${error.message}`));
44
+ child.on("close", (code) => {
45
+ clearTimeout(timer);
46
+ // The end of a test run (failures, summary) matters most.
47
+ const text = output.length > MAX_OUTPUT ? `[... ${output.length - MAX_OUTPUT} chars cut]\n${output.slice(-MAX_OUTPUT)}` : output;
48
+ resolve({ command, code, output: text.trim() });
49
+ });
50
+ });
51
+ }
52
+
53
+ export default {
54
+ name: "Run tests",
55
+ version: "0.1.0",
56
+ description: "Adds a run_tests tool and /test. Works out npm/pnpm/yarn/bun, cargo, go, pytest or make.",
57
+ systemPrompt: "After changing code, use run_tests to check your work, and fix any failures you caused.",
58
+
59
+ tools: [
60
+ {
61
+ // Running tests runs project code, so you approve each run.
62
+ definition: {
63
+ name: "run_tests",
64
+ description: "Run the project's test suite (or part of it) and return the output",
65
+ parameters: {
66
+ type: "object",
67
+ properties: { args: { type: "string", description: "Optional extra arguments, e.g. a test file or name filter" } },
68
+ },
69
+ },
70
+ async run(args, ctx) {
71
+ const { command, code, output } = await runTests(ctx.cwd, args.args?.trim());
72
+ return command ? `$ ${command}\nexit code ${code}\n${output}` : output;
73
+ },
74
+ },
75
+ ],
76
+
77
+ commands: [
78
+ {
79
+ name: "test",
80
+ description: "Run the tests: /test [args]",
81
+ async run(args, ctx) {
82
+ const { command, code, output } = await runTests(ctx.cwd, args.trim());
83
+ if (!command) return output;
84
+ const tail = output.split("\n").slice(-25).join("\n");
85
+ return `${code === 0 ? "✓ passed" : `✗ failed (exit ${code})`}: ${command}\n${tail}`;
86
+ },
87
+ },
88
+ ],
89
+ };
@@ -0,0 +1,81 @@
1
+ // Starter plugin: finds TODO / FIXME / HACK comments, for you (/todos) and the model (find_todos).
2
+ import { execFileSync } from "child_process";
3
+ import { readdirSync, readFileSync, statSync } from "fs";
4
+ import path from "path";
5
+
6
+ const MARKERS = /\b(TODO|FIXME|HACK|XXX)\b:?\s*(.*)/;
7
+ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "out", "target", "vendor", ".venv", "__pycache__", "coverage"]);
8
+ const MAX_FILE_BYTES = 1_000_000;
9
+ const MAX_RESULTS = 200;
10
+
11
+ /** Files git knows about (so .gitignore is respected), or a plain walk outside a repo. */
12
+ function listFiles(cwd, under) {
13
+ try {
14
+ const out = execFileSync("git", ["ls-files", "-co", "--exclude-standard", "--", under || "."], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
15
+ return out.split("\n").filter(Boolean);
16
+ } catch {
17
+ const files = [];
18
+ const walk = (dir) => {
19
+ for (const name of readdirSync(path.join(cwd, dir))) {
20
+ if (SKIP_DIRS.has(name) || files.length > 20_000) continue;
21
+ const rel = path.join(dir, name);
22
+ if (statSync(path.join(cwd, rel)).isDirectory()) walk(rel);
23
+ else files.push(rel);
24
+ }
25
+ };
26
+ walk(under || ".");
27
+ return files;
28
+ }
29
+ }
30
+
31
+ function findTodos(cwd, under) {
32
+ const found = [];
33
+ for (const file of listFiles(cwd, under)) {
34
+ if (found.length >= MAX_RESULTS) break;
35
+ try {
36
+ const full = path.join(cwd, file);
37
+ if (statSync(full).size > MAX_FILE_BYTES) continue;
38
+ const text = readFileSync(full, "utf-8");
39
+ if (text.includes("\0")) continue; // binary
40
+ text.split("\n").forEach((line, i) => {
41
+ const match = MARKERS.exec(line);
42
+ if (match && found.length < MAX_RESULTS) found.push(`${file.replaceAll("\\", "/")}:${i + 1} ${match[1]} ${match[2].trim()}`);
43
+ });
44
+ } catch {
45
+ // unreadable or deleted: skip
46
+ }
47
+ }
48
+ return found;
49
+ }
50
+
51
+ const report = (found) =>
52
+ found.length ? `${found.length}${found.length === MAX_RESULTS ? "+" : ""} found:\n${found.join("\n")}` : "No TODO/FIXME/HACK comments found.";
53
+
54
+ export default {
55
+ name: "TODOs",
56
+ version: "0.1.0",
57
+ description: "Lists TODO / FIXME / HACK comments. Adds a find_todos tool and /todos.",
58
+
59
+ tools: [
60
+ {
61
+ readOnly: true,
62
+ definition: {
63
+ name: "find_todos",
64
+ description: "List TODO, FIXME, HACK and XXX comments in the project (file:line and the note)",
65
+ parameters: {
66
+ type: "object",
67
+ properties: { path: { type: "string", description: "Optional folder to search, relative to the project" } },
68
+ },
69
+ },
70
+ run: (args, ctx) => report(findTodos(ctx.cwd, args.path)),
71
+ },
72
+ ],
73
+
74
+ commands: [
75
+ {
76
+ name: "todos",
77
+ description: "List TODO/FIXME comments: /todos [folder]",
78
+ run: (args, ctx) => report(findTodos(ctx.cwd, args.trim())),
79
+ },
80
+ ],
81
+ };