@noobdemon/noob-cli 1.6.0 → 1.7.1

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.6.0",
3
+ "version": "1.7.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/repl.js CHANGED
@@ -30,6 +30,8 @@ const SLASH = [
30
30
  { name: "/init", desc: "quét dự án & tạo noob.md" },
31
31
  { name: "/karpathy", desc: "rà soát code (Karpathy)" },
32
32
  { name: "/ultra", desc: "tự hành: tự nghĩ & làm nhiệm vụ" },
33
+ { name: "/agent", desc: "bật/tắt agent mode (spawn sub-agent)" },
34
+ { name: "/tokens", desc: "xem số token đã dùng phiên này" },
33
35
  { name: "/learn", desc: "chưng cất bài học vào noob.md" },
34
36
  { name: "/memory", desc: "xem bộ nhớ noob.md" },
35
37
  { name: "/login", desc: "đăng nhập bằng API key" },
@@ -129,7 +131,9 @@ export async function startRepl(opts = {}) {
129
131
  autoApprove: new Set(),
130
132
  yolo: !!opts.yolo || config.yoloDefault, // cờ --yolo HOẶC mặc định đã lưu (/auto-yolo)
131
133
  ultra: false, // chế độ tự hành (self-quest) đang chạy?
134
+ agentMode: false, // /agent on → cho phép spawn_agent / spawn_agents
132
135
  };
136
+ const tokenMeter = new TokenMeter();
133
137
 
134
138
  // Prompt = dòng trạng thái sống. Luôn phản ánh yolo + version theo thời gian
135
139
  // thực (vẽ lại mỗi lượt và ngay khi Shift+Tab), nên không cần gõ /status.
@@ -379,24 +383,39 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
379
383
  state.mode = "chat"; // tự hành chỉ chạy ở chế độ agent
380
384
  state.ultra = true;
381
385
  console.log(c.accent(" 🚀 " + t.ultraOn));
382
- let answer = await handle(ultraStart(goal));
383
- persist();
384
- let i = 0;
385
- // Lượt đầu = lập kế hoạch → KHÔNG xét hoàn thành. Mỗi vòng sau là một lượt
386
- // "tiếp tục" có cổng kiểm chứng; chỉ dừng khi token nằm ở CUỐI câu trả lời.
387
- while (state.ultra && i < MAX_QUESTS) {
388
- if (!answer) break; // lượt bị ngắt/ lỗi → dừng tự hành, đừng quay vô ích
389
- i++;
390
- console.log(c.accent(" ↻ " + t.ultraQuest(i)));
391
- answer = await handle(ultraContinue(goal));
386
+ // Mốc history TRƯỚC khi ULTRA bơm prompt — kết thúc thì cắt về để các lượt sau không bị "dính" mục tiêu cũ.
387
+ const baseLen = state.history.length;
388
+ try {
389
+ let answer = await handle(ultraStart(goal));
392
390
  persist();
393
- if (answer && ultraIsDone(answer)) {
394
- console.log(c.ok(" ✓ " + t.ultraDone));
395
- break;
391
+ let i = 0;
392
+ // Lượt đầu = lập kế hoạch → KHÔNG xét hoàn thành. Mỗi vòng sau là một lượt
393
+ // "tiếp tục" có cổng kiểm chứng; chỉ dừng khi token nằm ở CUỐI câu trả lời.
394
+ while (state.ultra && i < MAX_QUESTS) {
395
+ if (!answer) break; // lượt bị ngắt/ lỗi → dừng tự hành, đừng quay vô ích
396
+ i++;
397
+ console.log(c.accent(" ↻ " + t.ultraQuest(i)));
398
+ answer = await handle(ultraContinue(goal));
399
+ persist();
400
+ if (answer && ultraIsDone(answer)) {
401
+ console.log(c.ok(" ✓ " + t.ultraDone));
402
+ break;
403
+ }
396
404
  }
405
+ if (state.ultra && i >= MAX_QUESTS) console.log(c.tool(" " + t.ultraMax));
406
+ } finally {
407
+ // Dọn dấu vết ULTRA khỏi history (prompt khởi động, các lượt "tiếp tục",
408
+ // token <<ULTRA_DONE>>…) để các yêu cầu SAU đó không bị model coi như vẫn
409
+ // đang tự hành / vẫn theo đuổi mục tiêu cũ.
410
+ state.ultra = false;
411
+ if (state.history.length > baseLen) state.history.length = baseLen;
412
+ state.history.push({
413
+ role: "user",
414
+ content: "[Phiên ULTRA đã KẾT THÚC — mục tiêu cũ: " + goal + ". Bỏ qua mọi chỉ dẫn ULTRA trước đó, KHÔNG tự hành tiếp, KHÔNG phát token " + ULTRA_DONE + ". Chờ yêu cầu mới.]",
415
+ });
416
+ state.history.push({ role: "assistant", content: "OK, đã thoát chế độ ULTRA." });
417
+ persist();
397
418
  }
398
- if (state.ultra && i >= MAX_QUESTS) console.log(c.tool(" " + t.ultraMax));
399
- state.ultra = false;
400
419
  }
401
420
 
402
421
  // /init — quét dự án & sinh noob.md tổng quan (giống `/init` của Claude Code).
@@ -584,7 +603,9 @@ NGUYÊN TẮC:
584
603
  let timer = null;
585
604
  const tick = (label) => {
586
605
  const elapsed = ((Date.now() - t0) / 1000).toFixed(0);
587
- tui.status(c.dim(`${label}… ${elapsed}s`));
606
+ // Chèn token usage realtime (↑input ↓output) ngay cạnh spinner để người
607
+ // dùng thấy được số token đang cộng dồn trong khi model stream.
608
+ tui.status(c.dim(`${label}… ${elapsed}s · ${tokenMeter.format()}`));
588
609
  };
589
610
  const stopSpin = () => {
590
611
  if (timer) {
@@ -609,7 +630,8 @@ NGUYÊN TẮC:
609
630
  message: text,
610
631
  signal: abort.signal,
611
632
  onStatus: (s) => {
612
- if (!printer.started) tui.status(c.dim(" " + s));
633
+ // Kèm token meter để nhánh merge/search cũng thấy ↑input ↓output realtime.
634
+ if (!printer.started) tui.status(c.dim(` ${s} · ${tokenMeter.format()}`));
613
635
  },
614
636
  onDelta: (d) => {
615
637
  stopSpin();
@@ -634,10 +656,49 @@ NGUYÊN TẮC:
634
656
  startSpin(t.thinking);
635
657
  let printer = null;
636
658
 
659
+ const dispatchTool = async (name, input, depth = 0) => {
660
+ // spawn_agent / spawn_agents chỉ được phép khi agentMode bật; depth giới hạn
661
+ // bởi MAX_SUBAGENT_DEPTH để tránh đệ quy nổ.
662
+ if (name === "spawn_agent" || name === "spawn_agents") {
663
+ if (!state.agentMode)
664
+ return { allow: true, result: "ERROR: agent mode đang TẮT — gõ /agent on để bật trước khi spawn." };
665
+ if (depth >= MAX_SUBAGENT_DEPTH)
666
+ return { allow: true, result: `ERROR: đã đạt depth tối đa (${MAX_SUBAGENT_DEPTH}) — không spawn thêm.` };
667
+ const tasks = name === "spawn_agent" ? [input] : (Array.isArray(input?.agents) ? input.agents : []);
668
+ if (!tasks.length) return { allow: true, result: "ERROR: thiếu task cho sub-agent." };
669
+ stopSpin();
670
+ console.log(chalk.hex("#8b5cf6")(` ⊕ spawn ${tasks.length} sub-agent (depth ${depth + 1}/${MAX_SUBAGENT_DEPTH})`));
671
+ startSpin(t.thinking);
672
+ try {
673
+ const results = await Promise.all(tasks.map((task, i) =>
674
+ runSubAgent({
675
+ task: task?.task || task?.prompt || "",
676
+ context: task?.context || "",
677
+ depth: depth + 1,
678
+ model: state.model.id,
679
+ signal: abort.signal,
680
+ tokenMeter,
681
+ dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
682
+ onLog: (msg) => { stopSpin(); console.log(chalk.hex("#8b5cf6")(" " + msg)); startSpin(t.thinking); },
683
+ }).then((r) => `── sub-agent #${i + 1} ──\n${r}`).catch((e) => `── sub-agent #${i + 1} (LỖI) ──\n${e?.message || String(e)}`)
684
+ ));
685
+ return { allow: true, result: results.join("\n\n") };
686
+ } catch (err) {
687
+ return { allow: true, result: "ERROR sub-agent: " + (err?.message || String(err)) };
688
+ }
689
+ }
690
+ stopSpin();
691
+ const res = await execTool(name, input);
692
+ startSpin(t.thinking);
693
+ return res;
694
+ };
695
+
637
696
  const answer = await runAgent({
638
697
  history: state.history,
639
698
  model: state.model.id,
640
699
  signal: abort.signal,
700
+ tokenMeter,
701
+ extraToolsDoc: state.agentMode ? spawnAgentToolsDoc(0) : "",
641
702
  onStatus: () => tick(t.thinking),
642
703
  onSteer: () => {
643
704
  if (!pending.length) return [];
@@ -659,12 +720,7 @@ NGUYÊN TẮC:
659
720
  printer?.flush();
660
721
  }
661
722
  },
662
- onTool: async (name, input) => {
663
- stopSpin();
664
- const res = await execTool(name, input);
665
- startSpin(t.thinking);
666
- return res;
667
- },
723
+ onTool: (name, input) => dispatchTool(name, input, 0),
668
724
  });
669
725
 
670
726
  stopSpin();
@@ -773,6 +829,18 @@ NGUYÊN TẮC:
773
829
  state.yolo = !state.yolo;
774
830
  console.log((state.yolo ? c.err : c.ok)(" " + (state.yolo ? t.yoloOn : t.yoloOff)));
775
831
  break;
832
+ case "agent": {
833
+ const v = arg.toLowerCase();
834
+ if (v === "on" || v === "bật" || v === "bat") state.agentMode = true;
835
+ else if (v === "off" || v === "tắt" || v === "tat") state.agentMode = false;
836
+ else state.agentMode = !state.agentMode;
837
+ console.log((state.agentMode ? c.accent : c.dim)(" agent mode: " + (state.agentMode ? "BẬT (spawn_agent / spawn_agents khả dụng, depth tối đa " + MAX_SUBAGENT_DEPTH + ")" : "tắt")));
838
+ break;
839
+ }
840
+ case "tokens": {
841
+ console.log(c.dim(` tokens — input: ${tokenMeter.input.toLocaleString("vi-VN")} · output: ${tokenMeter.output.toLocaleString("vi-VN")} · tổng: ${tokenMeter.total.toLocaleString("vi-VN")} · ${tokenMeter.format()}`));
842
+ break;
843
+ }
776
844
  case "auto-yolo":
777
845
  case "autoyolo":
778
846
  await toggleAutoYolo();
package/src/subagent.js CHANGED
@@ -31,14 +31,17 @@ Ví dụ phân cấp: cha giao "build full app" → đẻ 1 sub-agent "build bac
31
31
 
32
32
  // Chạy một sub-agent. dispatchTool: hàm để thực thi tool con (chia sẻ với cha).
33
33
  // model: dùng chung model của cha. onLog: callback để log tiến độ ra UI cha.
34
- export async function runSubAgent({ task, context, model, signal, dispatchTool, depth = 1, onLog }) {
34
+ export async function runSubAgent({ task, context, model, signal, dispatchTool, depth = 1, onLog, tokenMeter }) {
35
35
  const sys = `Bạn là SUB-AGENT (depth=${depth}) được agent cha ủy thác MỘT nhiệm vụ cụ thể. Làm xong → trả lời NGẮN GỌN bằng Markdown tóm tắt KẾT QUẢ (file đã đụng, phát hiện, lỗi nếu có). Không tán gẫu. Không hỏi lại cha — tự quyết với thông tin được cấp.
36
36
 
37
37
  # NHIỆM VỤ
38
38
  ${task}
39
39
  ${context ? `\n# NGỮ CẢNH TỪ CHA\n${context}` : ""}`;
40
40
  const history = [{ role: "user", content: sys }];
41
- const meter = new TokenMeter();
41
+ // Dùng chung meter của cha nếu được truyền vào → token sub-agent cộng dồn
42
+ // vào tổng phiên. Nếu không có thì tự tạo cục bộ (giữ tương thích cũ).
43
+ const meter = tokenMeter || new TokenMeter();
44
+ const before = { input: meter.input, output: meter.output };
42
45
  onLog?.(`↳ sub-agent (depth=${depth}) bắt đầu: ${task.slice(0, 80)}${task.length > 80 ? "…" : ""}`);
43
46
  const result = await runAgent({
44
47
  history,
@@ -51,6 +54,8 @@ ${context ? `\n# NGỮ CẢNH TỪ CHA\n${context}` : ""}`;
51
54
  onDelta: () => {},
52
55
  onSteer: () => [],
53
56
  });
54
- onLog?.(`↳ sub-agent (depth=${depth}) xong (${meter.format()})`);
55
- return { result, tokens: { input: meter.input, output: meter.output } };
57
+ const used = { input: meter.input - before.input, output: meter.output - before.output };
58
+ onLog?.(`↳ sub-agent (depth=${depth}) xong (↑${used.input} ↓${used.output})`);
59
+ // Trả về string sạch để cha (model) đọc dễ. Token đã cộng vào meter rồi.
60
+ return result;
56
61
  }