@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.5.3",
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
- // Preferred: ```tool { ... } ```
180
- let m = text.match(/```tool\s*\n([\s\S]*?)```/);
181
- // Fallback: a ```json block that contains a "name" field.
182
- if (!m) {
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 spinner dự phòng suốt lượt kể cả lúc
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 { highlight, supportsLanguage } from "cli-highlight";
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
- function inline(s) {
51
- return s
52
- .replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t))
53
- .replace(/(^|[^*])\*(?!\*)(.+?)\*(?!\*)/g, (_, p, t) => p + chalk.italic(t))
54
- .replace(/`([^`]+)`/g, (_, t) => chalk.hex("#f59e0b")(t))
55
- .replace(/\[(.+?)\]\((.+?)\)/g, (_, t, u) => chalk.underline.cyan(t) + c.dim(` (${u})`));
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
+ // để màu, 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
- function renderCode(code, lang) {
59
- let body = code.replace(/\n$/, "");
60
- try {
61
- if (lang && supportsLanguage(lang)) body = highlight(body, { language: lang });
62
- else body = highlight(body, { ignoreIllegals: true });
63
- } catch {
64
- /* fall through to raw */
65
- }
66
- const label = c.dim((lang || "code") + " ▸ ");
67
- const lines = body.split("\n").map((l) => c.dim("│ ") + l);
68
- return "\n" + label + "\n" + lines.join("\n") + "\n";
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
- const out = [];
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") {