@noobdemon/noob-cli 1.5.3 → 1.5.5
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/package.json +3 -1
- package/src/agent.js +36 -12
- package/src/i18n.js +5 -0
- package/src/repl.js +57 -0
- package/src/tui.js +2 -1
- package/src/ui.js +39 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noobdemon/noob-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"chalk": "^5.4.1",
|
|
40
40
|
"cli-highlight": "^2.1.11",
|
|
41
41
|
"gradient-string": "^3.0.0",
|
|
42
|
+
"marked": "^15.0.12",
|
|
43
|
+
"marked-terminal": "^7.3.0",
|
|
42
44
|
"ora": "^8.2.0"
|
|
43
45
|
}
|
|
44
46
|
}
|
package/src/agent.js
CHANGED
|
@@ -175,24 +175,48 @@ function buildPrompt(history) {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
// Extract a single tool call from an assistant message, if present.
|
|
178
|
+
// NOTE: we do NOT match up to a closing ``` fence — write_file content routinely
|
|
179
|
+
// contains its own ```code``` fences (e.g. a README), and the first inner fence
|
|
180
|
+
// would close the block early and break the JSON. Instead, find the ```tool (or
|
|
181
|
+
// ```json) opener and brace-match the first balanced JSON object after it.
|
|
178
182
|
export function parseToolCall(text) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const j = text.match(/```json\s*\n([\s\S]*?)```/);
|
|
184
|
-
if (j && /"name"\s*:/.test(j[1])) m = j;
|
|
185
|
-
}
|
|
186
|
-
if (!m) return null;
|
|
187
|
-
try {
|
|
188
|
-
const obj = JSON.parse(m[1].trim());
|
|
183
|
+
for (const fence of ["tool", "json"]) {
|
|
184
|
+
const open = text.match(new RegExp("```" + fence + "[ \\t]*\\n"));
|
|
185
|
+
if (!open) continue;
|
|
186
|
+
const obj = extractJsonObject(text, open.index + open[0].length);
|
|
189
187
|
if (obj && typeof obj.name === "string") return { name: obj.name, input: obj.input || {} };
|
|
190
|
-
} catch {
|
|
191
|
-
/* malformed — treat as prose */
|
|
192
188
|
}
|
|
193
189
|
return null;
|
|
194
190
|
}
|
|
195
191
|
|
|
192
|
+
// Parse the first balanced {…} at/after `from`, tracking string literals and
|
|
193
|
+
// escapes so braces or backticks INSIDE string values don't throw off the depth
|
|
194
|
+
// count. Returns the parsed object, or null if malformed/truncated (unbalanced).
|
|
195
|
+
function extractJsonObject(s, from) {
|
|
196
|
+
const start = s.indexOf("{", from);
|
|
197
|
+
if (start === -1) return null;
|
|
198
|
+
let depth = 0, inStr = false, esc = false;
|
|
199
|
+
for (let i = start; i < s.length; i++) {
|
|
200
|
+
const ch = s[i];
|
|
201
|
+
if (inStr) {
|
|
202
|
+
if (esc) esc = false;
|
|
203
|
+
else if (ch === "\\") esc = true;
|
|
204
|
+
else if (ch === '"') inStr = false;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (ch === '"') inStr = true;
|
|
208
|
+
else if (ch === "{") depth++;
|
|
209
|
+
else if (ch === "}" && --depth === 0) {
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse(s.slice(start, i + 1));
|
|
212
|
+
} catch {
|
|
213
|
+
return null; // malformed JSON — treat as prose
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return null; // unbalanced (e.g. stream cut mid-block) — auto-continue finishes it
|
|
218
|
+
}
|
|
219
|
+
|
|
196
220
|
/**
|
|
197
221
|
* Run one agent turn (which may span several tool steps).
|
|
198
222
|
*
|
package/src/i18n.js
CHANGED
|
@@ -58,6 +58,7 @@ export const t = {
|
|
|
58
58
|
cmdChat: "/chat quay lại chế độ chat thường",
|
|
59
59
|
cmdYolo: "/yolo bật/tắt tự duyệt (hoặc nhấn Shift+Tab)",
|
|
60
60
|
cmdAutoYolo: "/auto-yolo lưu/bỏ yolo làm mặc định mỗi lần chạy (cần xác nhận)",
|
|
61
|
+
cmdInit: "/init quét dự án & tạo noob.md (tổng quan + quy ước, như Claude Code)",
|
|
61
62
|
cmdKarpathy: "/karpathy [path] rà soát code theo 4 nguyên tắc Karpathy (/kc)",
|
|
62
63
|
cmdUltra: "/ultra <mục tiêu> tự hành: noob tự nghĩ & tự làm nhiệm vụ tới khi xong (/u)",
|
|
63
64
|
cmdLearn: "/learn [ghi chú] chưng cất bài học của phiên vào noob.md",
|
|
@@ -108,6 +109,10 @@ export const t = {
|
|
|
108
109
|
ultraNeedGoal: "Cần mục tiêu. Dùng: /ultra <mô tả mục tiêu>",
|
|
109
110
|
ultraQuest: (n) => `tự nghĩ nhiệm vụ kế tiếp (vòng ${n})…`,
|
|
110
111
|
learning: "đang chưng cất bài học vào noob.md…",
|
|
112
|
+
initRunning: "đang quét dự án & soạn noob.md…",
|
|
113
|
+
initOverwriteWarn: (p) => `⚠ Đã có noob.md tại ${p}. /init sẽ ghi đè nội dung hiện tại.`,
|
|
114
|
+
initOverwriteConfirm: "Ghi đè? gõ 'y' để xác nhận, phím khác để huỷ › ",
|
|
115
|
+
initCancel: "Huỷ /init — giữ nguyên noob.md.",
|
|
111
116
|
memoryEmpty: (p) => `Chưa có noob.md. noob sẽ tự tạo ở: ${p}`,
|
|
112
117
|
memoryStat: (n) => ` · ${n} dòng / ~200`,
|
|
113
118
|
|
package/src/repl.js
CHANGED
|
@@ -25,6 +25,7 @@ const SLASH = [
|
|
|
25
25
|
{ name: "/chat", desc: "chế độ chat thường" },
|
|
26
26
|
{ name: "/yolo", desc: "bật/tắt tự duyệt" },
|
|
27
27
|
{ name: "/auto-yolo", desc: "lưu yolo làm mặc định (cần xác nhận)" },
|
|
28
|
+
{ name: "/init", desc: "quét dự án & tạo noob.md" },
|
|
28
29
|
{ name: "/karpathy", desc: "rà soát code (Karpathy)" },
|
|
29
30
|
{ name: "/ultra", desc: "tự hành: tự nghĩ & làm nhiệm vụ" },
|
|
30
31
|
{ name: "/learn", desc: "chưng cất bài học vào noob.md" },
|
|
@@ -396,6 +397,58 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
|
|
|
396
397
|
state.ultra = false;
|
|
397
398
|
}
|
|
398
399
|
|
|
400
|
+
// /init — quét dự án & sinh noob.md tổng quan (giống `/init` của Claude Code).
|
|
401
|
+
// Nếu noob.md đã có: hỏi xác nhận ghi đè trước khi giao việc cho model.
|
|
402
|
+
async function runInit() {
|
|
403
|
+
if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
|
|
404
|
+
const mem = loadMemory();
|
|
405
|
+
if (mem) {
|
|
406
|
+
console.log(c.err(" " + t.initOverwriteWarn(memoryPath())));
|
|
407
|
+
const ans = ((await ask(c.tool(" " + t.initOverwriteConfirm))) ?? "").trim().toLowerCase();
|
|
408
|
+
if (ans !== "y" && ans !== "yes" && ans !== "có") {
|
|
409
|
+
console.log(c.dim(" " + t.initCancel));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const prompt = `Hãy KHỞI TẠO bộ nhớ dự án \`noob.md\` ở thư mục gốc (giống cách Claude Code chạy /init).
|
|
414
|
+
|
|
415
|
+
QUY TRÌNH (làm bằng tool, không nói suông):
|
|
416
|
+
1. list_dir thư mục gốc để nhìn tổng quan.
|
|
417
|
+
2. Đọc các file quan trọng nếu có: package.json, README.md, pyproject.toml, requirements.txt, Cargo.toml, go.mod, tsconfig.json, vite.config*, next.config*, Makefile, Dockerfile, .editorconfig, .eslintrc*, .prettierrc*.
|
|
418
|
+
3. Lướt qua thư mục mã nguồn chính (src/, lib/, app/…) bằng list_dir/glob để hiểu kiến trúc — đừng đọc hết, chỉ đủ để nắm cấu trúc.
|
|
419
|
+
4. Nếu repo có test/build script, ghi lại lệnh CHÍNH XÁC (ví dụ \`npm test\`, \`pytest\`, \`cargo build\`).
|
|
420
|
+
|
|
421
|
+
SAU ĐÓ ghi \`noob.md\` bằng write_file (ghi đè nếu đã có) với cấu trúc CHÍNH XÁC sau:
|
|
422
|
+
|
|
423
|
+
# noob.md
|
|
424
|
+
|
|
425
|
+
## Tổng quan
|
|
426
|
+
- 2–5 gạch đầu dòng: tên dự án, mục đích, ngôn ngữ/runtime, framework chính.
|
|
427
|
+
|
|
428
|
+
## Lệnh thường dùng
|
|
429
|
+
- build: <lệnh hoặc "chưa rõ">
|
|
430
|
+
- test: <lệnh>
|
|
431
|
+
- run/dev: <lệnh>
|
|
432
|
+
- lint/format: <lệnh nếu có>
|
|
433
|
+
|
|
434
|
+
## Kiến trúc
|
|
435
|
+
- 3–8 gạch đầu dòng mô tả các thư mục/module chính & vai trò.
|
|
436
|
+
|
|
437
|
+
## Rules
|
|
438
|
+
- (để trống hoặc thêm quy ước ĐÃ CHỐT rút ra từ config: ví dụ "dùng ES modules (type: module)", "Node >=18"…)
|
|
439
|
+
|
|
440
|
+
## Notes
|
|
441
|
+
- (để trống — sẽ tự bổ sung sau khi học thêm trong quá trình làm việc)
|
|
442
|
+
|
|
443
|
+
NGUYÊN TẮC:
|
|
444
|
+
- Chỉ ghi sự thật rút ra từ file thật. KHÔNG bịa lệnh/quy ước không có cơ sở.
|
|
445
|
+
- Ngắn gọn (~80–150 dòng), mỗi ý một gạch đầu dòng.
|
|
446
|
+
- Khi xong, in 1 đoạn tóm tắt rất ngắn về những gì đã ghi vào noob.md.`;
|
|
447
|
+
console.log(c.tool(" 📋 " + t.initRunning));
|
|
448
|
+
await handle(prompt);
|
|
449
|
+
persist();
|
|
450
|
+
}
|
|
451
|
+
|
|
399
452
|
// /learn [ghi chú] — bắt noob chưng cất điều đáng nhớ của phiên vào noob.md.
|
|
400
453
|
async function runLearn(arg) {
|
|
401
454
|
if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
|
|
@@ -731,6 +784,9 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
|
|
|
731
784
|
case "u":
|
|
732
785
|
await runUltra(arg);
|
|
733
786
|
break;
|
|
787
|
+
case "init":
|
|
788
|
+
await runInit();
|
|
789
|
+
break;
|
|
734
790
|
case "learn":
|
|
735
791
|
await runLearn(arg);
|
|
736
792
|
break;
|
|
@@ -961,6 +1017,7 @@ function printHelp() {
|
|
|
961
1017
|
" " + t.cmdChat,
|
|
962
1018
|
" " + t.cmdYolo,
|
|
963
1019
|
" " + t.cmdAutoYolo,
|
|
1020
|
+
" " + t.cmdInit,
|
|
964
1021
|
" " + t.cmdKarpathy,
|
|
965
1022
|
" " + t.cmdUltra,
|
|
966
1023
|
" " + t.cmdLearn,
|
package/src/tui.js
CHANGED
|
@@ -74,11 +74,12 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
74
74
|
|
|
75
75
|
let liveOut = ""; // dòng output dở dang (chưa có '\n') hiện ngay trên thanh
|
|
76
76
|
let statusText = null; // text spinner khi đang nghĩ
|
|
77
|
-
// `busy` = một lượt/tool ĐANG chạy. Hiện
|
|
77
|
+
// `busy` = một lượt/tool ĐANG chạy. Hiện status bar suốt lượt kể cả lúc
|
|
78
78
|
// statusText tạm trống (vd model ngừng phun token giữa các bước) → người dùng
|
|
79
79
|
// LUÔN thấy rõ "đang chạy", không bị tưởng treo.
|
|
80
80
|
let busy = false;
|
|
81
81
|
let busyLabel = "";
|
|
82
|
+
let busyStartedAt = 0; // mốc thời gian để hiển thị elapsed
|
|
82
83
|
let frame = 0;
|
|
83
84
|
let frameTimer = null;
|
|
84
85
|
let prevRows = 0;
|
package/src/ui.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import gradient from "gradient-string";
|
|
3
3
|
import boxen from "boxen";
|
|
4
|
-
import {
|
|
4
|
+
import { supportsLanguage } from "cli-highlight";
|
|
5
|
+
import { marked } from "marked";
|
|
6
|
+
import { markedTerminal } from "marked-terminal";
|
|
5
7
|
import { PROVIDERS, providerColor } from "./models.js";
|
|
6
8
|
import { t } from "./i18n.js";
|
|
7
9
|
|
|
@@ -47,60 +49,46 @@ export function modelBadge(model) {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// ── Markdown → ANSI ────────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
// Dùng marked + marked-terminal. Một vài lưu ý từ source marked-terminal:
|
|
53
|
+
// * option `code` là STYLE fallback cho code block, không phải callback render.
|
|
54
|
+
// Body code block đã được cli-highlight xử lý sẵn → ta truyền highlightOptions
|
|
55
|
+
// để tô màu, và post-process để thêm viền trái `│`.
|
|
56
|
+
// * option `listitem` chạy AFTER bullet `*` được prepend, nên KHÔNG thêm `•` ở đây.
|
|
57
|
+
// Đổi bullet ở bước post-process.
|
|
58
|
+
// * option `href` không nên bọc ngoặc — wrapper tự thêm `(...)`.
|
|
59
|
+
const BULLET = c.accent("•");
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
marked.use(
|
|
62
|
+
markedTerminal(
|
|
63
|
+
{
|
|
64
|
+
width: Math.min(term(), 100),
|
|
65
|
+
reflowText: true,
|
|
66
|
+
tab: 2,
|
|
67
|
+
showSectionPrefix: false,
|
|
68
|
+
firstHeading: (s) => brand(s),
|
|
69
|
+
heading: chalk.hex("#a78bfa").bold,
|
|
70
|
+
blockquote: chalk.hex("#6b7280").italic,
|
|
71
|
+
strong: chalk.bold,
|
|
72
|
+
em: chalk.italic,
|
|
73
|
+
codespan: chalk.bgHex("#1f2937").hex("#fbbf24"),
|
|
74
|
+
hr: () => rule(),
|
|
75
|
+
link: chalk.hex("#06b6d4").underline,
|
|
76
|
+
href: chalk.hex("#9ca3af"),
|
|
77
|
+
code: chalk.hex("#f59e0b"),
|
|
78
|
+
},
|
|
79
|
+
{ ignoreIllegals: true },
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Post-process: đổi bullet `*` thành `•` màu accent, thêm viền `│` cho block code (4-space indent).
|
|
84
|
+
function prettify(s) {
|
|
85
|
+
return s
|
|
86
|
+
.replace(/^( *)\* /gm, (_, sp) => sp + BULLET + " ")
|
|
87
|
+
.replace(/^ {4}(.*)$/gm, (_, rest) => c.dim("│ ") + rest);
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
export function renderMarkdown(md) {
|
|
72
|
-
|
|
73
|
-
const lines = md.split("\n");
|
|
74
|
-
let i = 0;
|
|
75
|
-
while (i < lines.length) {
|
|
76
|
-
const line = lines[i];
|
|
77
|
-
const fence = line.match(/^```(\w*)\s*$/);
|
|
78
|
-
if (fence) {
|
|
79
|
-
const lang = fence[1];
|
|
80
|
-
const buf = [];
|
|
81
|
-
i++;
|
|
82
|
-
while (i < lines.length && !/^```\s*$/.test(lines[i])) buf.push(lines[i++]);
|
|
83
|
-
i++; // skip closing fence
|
|
84
|
-
out.push(renderCode(buf.join("\n"), lang));
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
let m;
|
|
88
|
-
if ((m = line.match(/^(#{1,6})\s+(.*)$/))) {
|
|
89
|
-
out.push(brand(chalk.bold(m[2])));
|
|
90
|
-
} else if (/^\s*[-*+]\s+/.test(line)) {
|
|
91
|
-
out.push(line.replace(/^(\s*)[-*+]\s+/, (_, sp) => sp + c.accent(" • ")) .replace(/^(\s*\S+\s)(.*)$/, (_, pre, rest) => pre + inline(rest)));
|
|
92
|
-
} else if ((m = line.match(/^(\s*)(\d+)\.\s+(.*)$/))) {
|
|
93
|
-
out.push(m[1] + c.accent(` ${m[2]}. `) + inline(m[3]));
|
|
94
|
-
} else if (/^\s*>\s?/.test(line)) {
|
|
95
|
-
out.push(c.dim("┃ ") + c.dim(inline(line.replace(/^\s*>\s?/, ""))));
|
|
96
|
-
} else if (/^\s*([-*_])\1{2,}\s*$/.test(line)) {
|
|
97
|
-
out.push(rule());
|
|
98
|
-
} else {
|
|
99
|
-
out.push(inline(line));
|
|
100
|
-
}
|
|
101
|
-
i++;
|
|
102
|
-
}
|
|
103
|
-
return out.join("\n");
|
|
91
|
+
return prettify(marked.parse(md || "")).trimEnd();
|
|
104
92
|
}
|
|
105
93
|
|
|
106
94
|
export function box(content, title, color = "#a78bfa") {
|