@noobdemon/noob-cli 1.12.0 → 1.12.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/src/skills.js CHANGED
@@ -70,3 +70,62 @@ export function listSkills() {
70
70
  const merged = new Set([...listIn(userSkillsDir()), ...listIn(PACKAGE_SKILLS_DIR)]);
71
71
  return [...merged].sort();
72
72
  }
73
+
74
+ // Parse YAML frontmatter tối giản ở đầu SKILL.md. Chỉ đọc các key đơn giản
75
+ // dạng `key: value` (không nested, không list). Trả về { meta, body }.
76
+ // VD đầu file:
77
+ // ---
78
+ // auto: true
79
+ // ---
80
+ // → meta = { auto: 'true' }, body = phần còn lại.
81
+ export function parseSkillFrontmatter(text) {
82
+ if (typeof text !== 'string') return { meta: {}, body: text || '' };
83
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
84
+ if (!m) return { meta: {}, body: text };
85
+ const meta = {};
86
+ for (const line of m[1].split(/\r?\n/)) {
87
+ const kv = line.match(/^\s*([A-Za-z_][\w-]*)\s*:\s*(.*?)\s*$/);
88
+ if (!kv) continue;
89
+ let v = kv[2];
90
+ // strip quotes nếu có
91
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
92
+ v = v.slice(1, -1);
93
+ }
94
+ meta[kv[1]] = v;
95
+ }
96
+ return { meta, body: text.slice(m[0].length) };
97
+ }
98
+
99
+ function isTruthy(v) {
100
+ if (v === true) return true;
101
+ if (typeof v !== 'string') return false;
102
+ return /^(true|yes|1|on)$/i.test(v.trim());
103
+ }
104
+
105
+ // Liệt kê các skill có `auto: true` ở frontmatter — sẽ được auto-inject vào
106
+ // system prompt mỗi turn (xem `agent.js::buildSystem`). Trả về [{name, body}].
107
+ // User skill (`<cwd>/skills/<name>`) override built-in cùng tên.
108
+ export function listAutoSkills() {
109
+ const out = [];
110
+ for (const name of listSkills()) {
111
+ const text = loadSkill(name);
112
+ if (!text) continue;
113
+ const { meta, body } = parseSkillFrontmatter(text);
114
+ if (isTruthy(meta.auto)) out.push({ name, body });
115
+ }
116
+ return out;
117
+ }
118
+
119
+ // Block để chèn vào system prompt. Trả về '' nếu không có skill auto nào.
120
+ export function autoSkillsBlock() {
121
+ const skills = listAutoSkills();
122
+ if (!skills.length) return '';
123
+ const parts = [
124
+ '# AUTO-ACTIVE SKILLS',
125
+ 'Các skill dưới đây luôn ON — áp dụng trong MỌI lượt khi tình huống khớp (xem mục "Khi nào kích hoạt" của từng skill). KHÔNG cần slash command.',
126
+ ];
127
+ for (const { name, body } of skills) {
128
+ parts.push('', `=== SKILL: ${name} ===`, body.trim(), `=== END SKILL: ${name} ===`);
129
+ }
130
+ return parts.join('\n');
131
+ }
package/src/tools.js CHANGED
@@ -2,6 +2,13 @@ import fs from 'node:fs/promises';
2
2
  import fssync from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
+ import {
6
+ createEntities as kgCreateEntities,
7
+ createRelations as kgCreateRelations,
8
+ addObservations as kgAddObservations,
9
+ searchNodes as kgSearchNodes,
10
+ formatGraphPretty as kgFormat,
11
+ } from './kg.js';
5
12
 
6
13
  const MAX_OUT = 30000; // hard cap on any tool result fed back to the model
7
14
  const cwd = () => process.cwd();
@@ -511,6 +518,47 @@ export const TOOLS = {
511
518
  bg.delete(id);
512
519
  return `Killed background process #${id} (${p.command}).`;
513
520
  },
521
+
522
+ // Knowledge graph tools — KHÔNG xin permission (user chọn tự do).
523
+ // Storage: <cwd>/.noob/kg.jsonl. Logic ở src/kg.js.
524
+ async kg_search({ query }, { signal } = {}) {
525
+ if (signal?.aborted) throw new Error('aborted');
526
+ const g = await kgSearchNodes(String(query || ''));
527
+ if (!g.entities.length && !g.relations.length) return `(không có kết quả cho "${query}")`;
528
+ return kgFormat(g);
529
+ },
530
+
531
+ async kg_add({ entities }, { signal } = {}) {
532
+ if (signal?.aborted) throw new Error('aborted');
533
+ if (!Array.isArray(entities) || entities.length === 0)
534
+ throw new Error('kg_add: entities phải là mảng không rỗng');
535
+ const created = await kgCreateEntities(entities);
536
+ if (!created.length) return 'Không có entity mới (đã trùng tên với entity sẵn có)';
537
+ return `Đã thêm ${created.length} entity: ${created.map((e) => e.name).join(', ')}`;
538
+ },
539
+
540
+ async kg_link({ relations }, { signal } = {}) {
541
+ if (signal?.aborted) throw new Error('aborted');
542
+ if (!Array.isArray(relations) || relations.length === 0)
543
+ throw new Error('kg_link: relations phải là mảng không rỗng');
544
+ const created = await kgCreateRelations(relations);
545
+ if (!created.length) return 'Không có relation mới (đã trùng triple from+to+type)';
546
+ return (
547
+ `Đã thêm ${created.length} relation:\n` +
548
+ created.map((r) => ` ${r.from} --${r.relationType}--> ${r.to}`).join('\n')
549
+ );
550
+ },
551
+
552
+ async kg_obs({ entityName, contents }, { signal } = {}) {
553
+ if (signal?.aborted) throw new Error('aborted');
554
+ if (!entityName) throw new Error('kg_obs: thiếu entityName');
555
+ if (!Array.isArray(contents) || contents.length === 0)
556
+ throw new Error('kg_obs: contents phải là mảng không rỗng');
557
+ const out = await kgAddObservations([{ entityName, contents }]);
558
+ const added = out[0]?.addedObservations || [];
559
+ if (!added.length) return `Không có observation mới cho '${entityName}' (đã trùng)`;
560
+ return `Đã thêm ${added.length} observation cho '${entityName}':\n` + added.map((o) => ` - ${o}`).join('\n');
561
+ },
514
562
  };
515
563
 
516
564
  // Khớp old_string theo KHỐI DÒNG, bỏ qua khác biệt CRLF và khoảng trắng CUỐI
@@ -631,6 +679,14 @@ export function describe(name, input) {
631
679
  return `↳ sub-agent: ${String(input.task || '').slice(0, 80)}`;
632
680
  case 'spawn_agents':
633
681
  return `↳ ${(input.tasks || []).length} sub-agent song song`;
682
+ case 'kg_search':
683
+ return `kg search "${input.query || ''}"`;
684
+ case 'kg_add':
685
+ return `kg add ${(input.entities || []).length} entity`;
686
+ case 'kg_link':
687
+ return `kg link ${(input.relations || []).length} relation`;
688
+ case 'kg_obs':
689
+ return `kg obs '${input.entityName || ''}' (${(input.contents || []).length} fact)`;
634
690
  default:
635
691
  return name;
636
692
  }
package/src/tui.js CHANGED
@@ -75,7 +75,9 @@ function wrapText(text, width, maxLines) {
75
75
  }
76
76
  return lines.map(close);
77
77
  }
78
- const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
78
+ // Ember pulse: nhịp lửa lan ra/thu vào, ấm hơn Braille mặc định mà vẫn nhẹ.
79
+ // 8 frame chia 2 pha: lan rộng (·∘○◌◍) → thu lại. Phù hợp accent ember.
80
+ const FRAMES = ['·', '∘', '○', '◌', '◍', '◌', '○', '∘'];
79
81
  function truncStr(s, max) {
80
82
  if (!s) return '';
81
83
  return s.length > max ? s.slice(0, max - 1) + '…' : s;
@@ -789,10 +791,26 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
789
791
  draw();
790
792
  },
791
793
  read(label) {
794
+ // Lưu label CŨ trước khi đổi sang prompt mới (vd 'cho phép? [y/n]').
795
+ // Sau khi user trả lời xong (waiter resolve), restore lại label cũ +
796
+ // redraw — nếu không, dòng prompt tạm sẽ stick trên thanh nhập đến hết
797
+ // turn (mọi draw() sau đều render label tạm).
798
+ const prevLabel = promptLabel;
792
799
  if (label != null) promptLabel = label;
793
- if (queue.length) return Promise.resolve(queue.shift());
800
+ if (queue.length) {
801
+ const v = queue.shift();
802
+ promptLabel = prevLabel;
803
+ draw();
804
+ return Promise.resolve(v);
805
+ }
794
806
  draw();
795
- return new Promise((res) => (waiter = res));
807
+ return new Promise((res) => {
808
+ waiter = (val) => {
809
+ promptLabel = prevLabel;
810
+ draw();
811
+ res(val);
812
+ };
813
+ });
796
814
  },
797
815
  close() {
798
816
  try {
package/src/ui.js CHANGED
@@ -1,81 +1,95 @@
1
1
  import chalk from 'chalk';
2
2
  import gradient from 'gradient-string';
3
3
  import boxen from 'boxen';
4
- import { supportsLanguage } from 'cli-highlight';
5
4
  import { marked } from 'marked';
6
5
  import { markedTerminal } from 'marked-terminal';
7
6
  import { PROVIDERS, providerColor } from './models.js';
8
7
  import { t } from './i18n.js';
9
8
 
10
- const BRAND = ['#a78bfa', '#3b82f6', '#06b6d4'];
11
- const brand = gradient(BRAND);
12
-
9
+ // ── THEME: "Terminal Ember" ─────────────────────────────────────────────────
10
+ // Một accent thật mạnh (ember-orange) trên nền warm-gray. KHÔNG dùng
11
+ // purple/blue/cyan gradient — đó là AI-slop cliché. Đây là CLI lập trình:
12
+ // muốn cảm giác xưởng rèn / lò than về đêm, không phải landing page SaaS.
13
+ //
14
+ // ember (accent) #ff7a45 — tia lửa: prompt cursor, badge, link, brand mark
15
+ // amber (warn) #f0a868 — tool call, cảnh báo nhẹ, inline code
16
+ // moss (ok) #84a98c — success xanh xám trầm, không neon
17
+ // rust (err) #c0524b — đỏ rỉ, không chói
18
+ // user (prose) #e8e3d8 — trắng ngà (paper), prose chính
19
+ // ai (model) #f0a868 — câu thoại model = amber để phân biệt user
20
+ // dim (mute) #6b6359 — warm gray, KHÔNG xám lạnh #6b7280
13
21
  export const c = {
14
- dim: chalk.hex('#6b7280'),
15
- user: chalk.hex('#a78bfa'),
16
- ai: chalk.hex('#06b6d4'),
17
- tool: chalk.hex('#f59e0b'),
18
- ok: chalk.hex('#10b981'),
19
- err: chalk.hex('#ef4444'),
20
- accent: chalk.hex('#3b82f6'),
22
+ dim: chalk.hex('#6b6359'),
23
+ user: chalk.hex('#e8e3d8'),
24
+ ai: chalk.hex('#f0a868'),
25
+ tool: chalk.hex('#f0a868'),
26
+ ok: chalk.hex('#84a98c'),
27
+ err: chalk.hex('#c0524b'),
28
+ accent: chalk.hex('#ff7a45'),
21
29
  };
22
30
 
31
+ // Single-tone "gradient" giữ tương thích API gradient-string nội bộ. Banner
32
+ // dùng 2-stop ember→amber rất hẹp để không thành rainbow.
33
+ const brandGrad = gradient(['#ff7a45', '#f0a868']);
34
+ const brand = (s) => brandGrad(s);
35
+
23
36
  const term = () => process.stdout.columns || 80;
24
37
 
38
+ // ── Banner ─────────────────────────────────────────────────────────────────
39
+ // Wordmark heavy box-drawing 3 dòng — mảnh hơn block-letter ASCII thông thường,
40
+ // có cấu trúc rõ (gạch ngang heavy ┏━┓ ┗━┛). Sidebar ember ▌ ở mép trái làm
41
+ // "signature" — đập vào mắt ngay khi nhìn. Meta line dưới: ver · tagline,
42
+ // phân cách bằng ◆ ember nhỏ thay vì dấu · xám.
25
43
  export function banner() {
26
- const art = [
27
- ' ███╗ ██╗ ██████╗ ██████╗ ██████╗ ',
28
- ' ████╗ ██║██╔═══██╗██╔═══██╗██╔══██╗',
29
- ' ██╔██╗ ██║██║ ██║██║ ██║██████╔╝',
30
- ' ██║╚██╗██║██║ ██║██║ ██║██╔══██╗',
31
- ' ██║ ╚████║╚██████╔╝╚██████╔╝██████╔╝',
32
- ' ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ',
33
- ].join('\n');
34
- console.log('\n' + brand.multiline(art));
35
- console.log(c.dim(' ') + brand('Noob Demon') + c.dim(' · ' + t.tagline + '\n'));
44
+ const sidebar = chalk.hex('#ff7a45')('▌');
45
+ const wordmark = [
46
+ '┏┓╻┏━┓┏━┓┏┓ ',
47
+ '┃┗┫┃ ┃┃ ┃┣┻┓',
48
+ '╹ ╹┗━┛┗━┛┗━┛',
49
+ ].map((row) => sidebar + ' ' + brand(row)).join('\n');
50
+ const diamond = chalk.hex('#ff7a45')('◆');
51
+ const tag = chalk.hex('#e8e3d8').italic(t.tagline);
52
+ const meta = ' ' + c.dim('agentic terminal') + ' ' + diamond + ' ' + tag;
53
+ console.log('\n' + wordmark);
54
+ console.log(meta + '\n');
36
55
  }
37
56
 
38
57
  function rule(label = '') {
39
58
  const w = Math.min(term(), 100);
40
- if (!label) return c.dim(''.repeat(w));
41
- const head = `── ${label} `;
42
- return c.dim(head + ''.repeat(Math.max(0, w - head.length)));
59
+ if (!label) return c.dim(''.repeat(w));
60
+ const head = `┄┄ ${label} `;
61
+ return c.dim(head + ''.repeat(Math.max(0, w - head.length)));
43
62
  }
44
63
 
45
64
  export function modelBadge(model) {
46
65
  const color = providerColor(model.provider);
47
66
  const prov = PROVIDERS[model.provider]?.name || model.provider;
48
- return chalk.hex(color)('●') + ' ' + chalk.bold(model.name) + c.dim(` (${prov} · ${model.tier})`);
67
+ return (
68
+ chalk.hex(color)('◆') +
69
+ ' ' +
70
+ chalk.hex('#e8e3d8').bold(model.name) +
71
+ c.dim(` ${prov} · ${model.tier}`)
72
+ );
49
73
  }
50
74
 
51
75
  // ── Markdown → ANSI ────────────────────────────────────────────────────────
52
- // Dùng marked + marked-terminal. Một vài lưu ý từ source marked-terminal:
53
- // * option `code` 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('•');
60
-
61
- // Heading scale theo level — marked-terminal gọi `heading` với (text, level).
62
- // h1 → brand gradient (chỉ dùng cho firstHeading thực ra, nhưng giữ fallback).
63
- // h2 → tím đậm (section chính, vd `## Đề xuất ưu tiên hàng đầu`).
64
- // h3 → cyan đậm (sub-section, vd `### 10. Sub-project ...`).
65
- // h4+ → xám sáng đậm để vẫn nổi nhưng không cướp spotlight của h2/h3.
76
+ // Heading scale: h1 brand, h2 ember, h3 amber, h4+ dim đậm. TẤT CẢ trong cùng
77
+ // family ember-amber không nhảy hue (lavender→cyan) như cũ.
66
78
  const HEADING_BY_LEVEL = {
67
79
  1: (s) => brand(s),
68
- 2: chalk.hex('#a78bfa').bold,
69
- 3: chalk.hex('#06b6d4').bold,
70
- 4: chalk.hex('#9ca3af').bold,
71
- 5: chalk.hex('#9ca3af').bold,
72
- 6: chalk.hex('#9ca3af').bold,
80
+ 2: chalk.hex('#ff7a45').bold,
81
+ 3: chalk.hex('#f0a868').bold,
82
+ 4: chalk.hex('#bfb6a8').bold,
83
+ 5: chalk.hex('#bfb6a8').bold,
84
+ 6: chalk.hex('#bfb6a8').bold,
73
85
  };
74
86
  function headingStyler(text, level) {
75
87
  const fn = HEADING_BY_LEVEL[level] || HEADING_BY_LEVEL[2];
76
88
  return fn(text);
77
89
  }
78
90
 
91
+ const BULLET = c.accent('›');
92
+
79
93
  marked.use(
80
94
  markedTerminal(
81
95
  {
@@ -85,49 +99,38 @@ marked.use(
85
99
  showSectionPrefix: false,
86
100
  firstHeading: (s) => brand(s),
87
101
  heading: headingStyler,
88
- blockquote: chalk.hex('#6b7280').italic,
89
- // **bold** vàng đậm để các nhãn `**Vấn đề/cơ hội:**`, `**Đề xuất:**`,
90
- // `**Lợi ích:**`, `**P0**`, `**#1 ...**` đập vào mắt thay lẫn prose.
91
- strong: chalk.hex('#fbbf24').bold,
92
- em: chalk.italic,
93
- // `inline code` (backtick): xanh pastel không bg tách rõ khỏi prose
94
- // không chói như highlight bg cũ. Dùng cho path:line, tên file, tên hàm.
95
- codespan: chalk.hex('#34d399'),
102
+ blockquote: chalk.hex('#bfb6a8').italic,
103
+ // **bold** = ember sáng chỉ MỘT accent xuyên suốt prose. Nhãn quan trọng
104
+ // ("P0", "Đề xuất:", "#1 ...") tự nổi không cần thêm hue.
105
+ strong: chalk.hex('#ff7a45').bold,
106
+ em: chalk.hex('#e8e3d8').italic,
107
+ // `inline code` (backtick): amber pastel. Tách khỏi prose paper-white
108
+ // không đụng accent ember (giữ ember cho điểm nhấn THẬT).
109
+ codespan: chalk.hex('#f0a868'),
96
110
  hr: () => rule(),
97
- link: chalk.hex('#06b6d4').underline,
98
- href: chalk.hex('#9ca3af'),
99
- code: chalk.hex('#f59e0b'),
111
+ link: chalk.hex('#ff7a45').underline,
112
+ href: chalk.hex('#bfb6a8'),
113
+ code: chalk.hex('#f0a868'),
100
114
  },
101
115
  { ignoreIllegals: true }
102
116
  )
103
117
  );
104
118
 
105
- // Post-process:
106
- // 1. Bullet `*` → `•` màu accent.
107
- // 2. Block code (4-space indent) → viền trái `│`.
108
- // 3. Inline `**bold**` và backtick `code` BÊN TRONG list item: marked-terminal
109
- // v7 + marked v15 không chạy inline parser cho list children → ta tự parse
110
- // sau cùng. Chạy SAU bước 1 để bullet đã đổi rồi.
111
- const INLINE_BOLD = chalk.hex('#fbbf24').bold;
112
- const INLINE_CODE = chalk.hex('#34d399');
119
+ const INLINE_BOLD = chalk.hex('#ff7a45').bold;
120
+ const INLINE_CODE = chalk.hex('#f0a868');
113
121
  export function renderInline(text) {
114
- // `code` trước (để `**foo**` trong code không bị parse). Backtick không được
115
- // chứa newline; cho phép escape \` nếu cần.
116
122
  let out = text.replace(/`([^`\n]+)`/g, (_, code) => INLINE_CODE(code));
117
- // **bold** — non-greedy, không chứa newline. Cho phép nội dung là `code` đã ANSI.
118
123
  out = out.replace(/\*\*([^*\n]+?)\*\*/g, (_, b) => INLINE_BOLD(b));
119
124
  return out;
120
125
  }
121
126
 
122
- // Style heading prefix theo level cho stream printer (không qua marked).
123
- // `## foo` → ANSI styled foo, `### bar` → khác cấp. Trả về null nếu không phải heading.
124
127
  const HEADING_STYLE_BY_LEVEL = {
125
128
  1: brand,
126
- 2: chalk.hex('#a78bfa').bold,
127
- 3: chalk.hex('#06b6d4').bold,
128
- 4: chalk.hex('#9ca3af').bold,
129
- 5: chalk.hex('#9ca3af').bold,
130
- 6: chalk.hex('#9ca3af').bold,
129
+ 2: chalk.hex('#ff7a45').bold,
130
+ 3: chalk.hex('#f0a868').bold,
131
+ 4: chalk.hex('#bfb6a8').bold,
132
+ 5: chalk.hex('#bfb6a8').bold,
133
+ 6: chalk.hex('#bfb6a8').bold,
131
134
  };
132
135
  export function renderHeadingLine(line) {
133
136
  const m = line.match(/^(#{1,6})\s+(.+)$/);
@@ -138,14 +141,14 @@ export function renderHeadingLine(line) {
138
141
  return fn(renderInline(text));
139
142
  }
140
143
 
141
- // Đổi bullet đầu dòng `* `, `- `, `+ ` `• ` màu accent. Numbered list giữ nguyên.
144
+ // Bullet đầu dòng `*`, `-`, `+``›` ember. Không dùng `•` (AI default).
142
145
  export function renderBulletPrefix(line) {
143
146
  return line.replace(/^( *)[*\-+] /, (_, sp) => sp + BULLET + ' ');
144
147
  }
145
148
  function prettify(s) {
146
149
  return s
147
150
  .replace(/^( *)\* /gm, (_, sp) => sp + BULLET + ' ')
148
- .replace(/^ {4}(.*)$/gm, (_, rest) => c.dim(' ') + rest)
151
+ .replace(/^ {4}(.*)$/gm, (_, rest) => c.dim(' ') + rest)
149
152
  .split('\n')
150
153
  .map(renderInline)
151
154
  .join('\n');
@@ -155,30 +158,29 @@ export function renderMarkdown(md) {
155
158
  return prettify(marked.parse(md || '')).trimEnd();
156
159
  }
157
160
 
158
- // Format quota usage cho status bar: "4.7k/5k req" với màu theo % used.
159
- // < 70% → dim (xám)
160
- // 70-85% → tool (vàng cam)
161
- // > 85% → err (đỏ)
162
- // Plan admin (limit = -1 hoặc 0 với unlimited flag) → trả null (không hiện).
161
+ // ── formatQuota ────────────────────────────────────────────────────────────
162
+ // Cùng palette: dim → amber → ember → rust theo % used. Không nhảy hue lạ.
163
163
  function fmtK(n) {
164
164
  if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
165
165
  return String(n);
166
166
  }
167
167
  export function formatQuota(u) {
168
168
  if (!u || !u.ok) return null;
169
- if (u.plan === 'admin') return null; // unlimited — đỡ rác status bar.
169
+ if (u.plan === 'admin') return null;
170
170
  const limit = Number(u.limit);
171
171
  const remaining = Number(u.remaining);
172
172
  if (!Number.isFinite(limit) || !Number.isFinite(remaining) || limit <= 0) return null;
173
173
  const used = Math.max(0, limit - remaining);
174
174
  const pct = (used / limit) * 100;
175
- const label = `${fmtK(remaining)}/${fmtK(limit)} req`;
175
+ const label = `${fmtK(remaining)}/${fmtK(limit)}`;
176
176
  if (pct > 85) return c.err(label);
177
- if (pct > 70) return c.tool(label);
177
+ if (pct > 70) return c.accent(label);
178
+ if (pct > 50) return c.tool(label);
178
179
  return c.dim(label);
179
180
  }
180
181
 
181
- export function box(content, title, color = '#a78bfa') {
182
+ // ── Box ────────────────────────────────────────────────────────────────────
183
+ export function box(content, title, color = '#ff7a45') {
182
184
  return boxen(content, {
183
185
  title,
184
186
  titleAlignment: 'left',