@noobdemon/noob-cli 1.12.1 → 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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  Tất cả thay đổi đáng kể của `@noobdemon/noob-cli` được ghi vào file này.
4
4
 
5
+ ## [1.12.2] - 2026-06-12
6
+
7
+ ### Added
8
+ - **Knowledge Graph CLI feature** (`src/kg.js` + `src/tools.js` + `src/repl.js`): port logic từ `mcp-knowledge-graph` (Shane Holloman) thành chức năng nội bộ, KHÔNG dùng MCP server. Storage `<cwd>/.noob/kg.jsonl` per-project, marker riêng `{"type":"_noob_kg","version":1}`. Slash `/kg` 10 sub-cmd: `list|path|add|obs|link|unlink|unobs|get|search|rm`. 4 tool agent (no permission, user chọn tự do): `kg_search`/`kg_add`/`kg_link`/`kg_obs`. Dedupe: entity by `name`, relation by triple (from,to,type), obs by string equal. Smoke `scripts/smoke-kg.mjs` 45/45 pass.
9
+ - **Tách agent dispatcher** (`src/repl/agent-dispatch.js`): factory `createAgentDispatcher(deps)` xử lý `spawn_agent`/`spawn_agents` (sub-agent recursion + workflow journal cache) và forward tool thường sang `execTool`. Inject `runSubAgent`/`findModel`/`journal` cho test (smoke `scripts/smoke-dispatch.mjs`). Giảm closure coupling trong `startRepl`.
10
+
11
+ ### Removed
12
+ - **Skill `knowledge-graph` cũ** trong `skills/`: gỡ folder skill (convention markdown) — thay bằng feature CLI thật (`src/kg.js`) có structured storage + tool API rõ ràng hơn. Helper auto-skill (`parseSkillFrontmatter`/`listAutoSkills`/`autoSkillsBlock`) trong `src/skills.js` GIỮ NGUYÊN để skill khác có thể tận dụng pattern này sau.
13
+
5
14
  ## [1.12.1] - 2026-06-11
6
15
 
7
16
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.12.1",
3
+ "version": "1.12.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/agent.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import os from 'node:os';
2
2
  import { stream } from './api.js';
3
3
  import { loadMemory, memoryStats } from './memory.js';
4
+ import { autoSkillsBlock } from './skills.js';
4
5
  import { listRoots } from './tools.js';
5
6
  import { t } from './i18n.js';
6
7
  import { countTokens } from './tokens.js';
@@ -413,6 +414,10 @@ function relTime(ts) {
413
414
  // → chèn ngay sau memoryBlock() để model "thấy" lịch sử dù chưa /resume.
414
415
  export function buildSystem(history, extraToolsDoc, goal, recentSessions) {
415
416
  const parts = [SYSTEM, '', memoryBlock()];
417
+ // Auto-active skills (frontmatter `auto: true`) — luôn ON, không cần slash.
418
+ // Đặt sau memoryBlock để model thấy convention skill TRƯỚC khi vào tool/goal.
419
+ const autoSkills = autoSkillsBlock();
420
+ if (autoSkills) parts.push('', autoSkills);
416
421
  if (recentSessions && recentSessions.length) {
417
422
  parts.push('', recentSessionsBlock(recentSessions));
418
423
  }
package/src/api.js CHANGED
@@ -386,6 +386,25 @@ async function streamOnce({
386
386
  retryable: true,
387
387
  });
388
388
  }
389
+ // Network drop thô từ Node fetch (TypeError: fetch failed) hoặc lỗi tầng
390
+ // socket (ECONNRESET / ECONNREFUSED / ETIMEDOUT / EAI_AGAIN / ENETUNREACH /
391
+ // UND_ERR_*). Tự throw nguyên dạng → streamWithRetry KHÔNG retry (vì check
392
+ // err.name === 'ApiError'). Bọc thành ApiError retryable để được backoff.
393
+ const causeCode = err?.cause?.code || err?.code;
394
+ const isNetworkDrop =
395
+ err?.name === 'TypeError' && /fetch failed/i.test(err?.message || '');
396
+ const isSocketErr =
397
+ typeof causeCode === 'string' &&
398
+ /^(ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|ENOTFOUND|EPIPE|UND_ERR_)/i.test(
399
+ causeCode
400
+ );
401
+ if (isNetworkDrop || isSocketErr) {
402
+ const detail = causeCode || err?.cause?.message || err?.message || 'network';
403
+ throw new ApiError(`network: ${detail}`, {
404
+ code: 'network_drop',
405
+ retryable: true,
406
+ });
407
+ }
389
408
  throw err;
390
409
  } finally {
391
410
  clearInterval(idleTimer);
@@ -426,4 +445,33 @@ export function resetUsageCache() {
426
445
  _usageCache = { key: null, at: 0, data: null };
427
446
  }
428
447
 
448
+ // Pure logic check quota soft-cap. Trả null (không warn) hoặc
449
+ // { severity: 'warn'|'critical', message } để caller render + confirm.
450
+ // Tách khỏi UI để smoke test được — KHÔNG đụng cache, không gọi API.
451
+ //
452
+ // Quy tắc:
453
+ // - admin / status != active / data thiếu → null (skip)
454
+ // - trial: remaining ≤ 10 → critical; ≤ 50 → warn
455
+ // - khác (pro/proplus/ultra): remaining/limit < 10% → warn
456
+ export function evaluateQuotaWarning(u, label, t) {
457
+ if (!u || !u.ok) return null;
458
+ if (u.plan === 'admin') return null;
459
+ if (u.status && u.status !== 'active') return null;
460
+ const remaining = Number(u.remaining);
461
+ if (!Number.isFinite(remaining)) return null;
462
+ if (u.plan === 'trial') {
463
+ if (remaining <= 10)
464
+ return { severity: 'critical', message: t.quotaWarnTrialCritical(label, remaining) };
465
+ if (remaining <= 50)
466
+ return { severity: 'warn', message: t.quotaWarnTrialLow(label, remaining) };
467
+ return null;
468
+ }
469
+ const limit = Number(u.limit);
470
+ if (!Number.isFinite(limit) || limit <= 0) return null;
471
+ const pct = (remaining / limit) * 100;
472
+ if (pct < 10)
473
+ return { severity: 'warn', message: t.quotaWarnLow(label, remaining, limit, pct.toFixed(1)) };
474
+ return null;
475
+ }
476
+
429
477
  export { ApiError };
package/src/i18n.js CHANGED
@@ -18,6 +18,10 @@ export const t = {
18
18
  steerWillInject: (txt) => `💬 sẽ chèn cho AI ở bước tới: ${txt}`,
19
19
  steerInject: (txt) => `💬 đã chèn cho AI: ${txt}`,
20
20
  permRetry: '→ gõ y (đồng ý) · n (từ chối) · a (luôn cho phép)',
21
+ permRetryExtended:
22
+ '→ gõ y (đồng ý) · n (từ chối) · a (luôn cho phép) · t (hết turn) · f (file này)',
23
+ permGrantedTurn: (name) => `(đã bật auto-approve ${name} cho turn này)`,
24
+ permGrantedFile: (name, p) => `(đã bật auto-approve ${name} cho file ${p} hết phiên)`,
21
25
 
22
26
  // auth
23
27
  notLoggedIn:
@@ -38,6 +42,18 @@ export const t = {
38
42
  trialLeft: (n) => `${n} lượt dùng thử còn lại`,
39
43
  windowInfo: (used, limit) => `${used}/${limit} trong cửa sổ 5 giờ`,
40
44
 
45
+ // quota soft-cap warnings (trước khi start ULTRA / loop / workflow)
46
+ quotaWarnTitle: 'Cảnh báo hạn mức',
47
+ quotaWarnLow: (label, remaining, limit, pct) =>
48
+ `${label} sắp ăn nhiều request mà key chỉ còn ${remaining}/${limit} (${pct}%). Tiếp tục có thể đốt sạch quota.`,
49
+ quotaWarnTrialLow: (label, remaining) =>
50
+ `${label} sắp ăn nhiều request mà key TRIAL chỉ còn ${remaining} lượt. Hết là key dead, không hồi.`,
51
+ quotaWarnTrialCritical: (label, remaining) =>
52
+ `⚠ NGUY HIỂM: ${label} chuẩn bị chạy mà key TRIAL chỉ còn ${remaining} lượt. Hết là key dead vĩnh viễn.`,
53
+ quotaConfirm: 'Vẫn tiếp tục? (y/n)',
54
+ quotaCancelled: (label) => `đã huỷ ${label} để giữ quota.`,
55
+ quotaYoloBypass: '(yolo: bỏ qua xác nhận, vẫn cảnh báo)',
56
+
41
57
  // errors (gateway codes → VN)
42
58
  errMissingKey: 'Thiếu API key. Chạy: noob login <key>',
43
59
  errInvalidKey: 'API key không hợp lệ.',
@@ -73,7 +89,18 @@ export const t = {
73
89
  cmdLoop: '/loop <interval> <task> chạy task lặp lại (vd /loop 10m triage); /loop stop để dừng',
74
90
  cmdLearn: '/learn [ghi chú] chưng cất bài học của phiên vào noob.md',
75
91
  cmdCompact: '/compact tóm tắt phiên ngay để gọn ngữ cảnh (giữ trí nhớ dài hạn)',
76
- cmdMemory: '/memory xem bộ nhớ noob.md (/mem)',
92
+ cmdMemory: '/memory [stats] xem bộ nhớ noob.md hoặc thống kê size + cảnh báo bloat (/mem)',
93
+ memoryStatsHeader: '📝 Memory stats',
94
+ memoryStatsMain: (lines, bytes, rules, notes, ago) =>
95
+ ` noob.md : ${lines} dòng · ${bytes} · ${rules} rules · ${notes} notes · cập nhật ${ago}`,
96
+ memoryStatsArchive: (lines, bytes, ago) =>
97
+ ` noob-archive.md : ${lines} dòng · ${bytes} · cập nhật ${ago} (không inject vào prompt)`,
98
+ memoryStatsArchiveMissing: ' noob-archive.md : (chưa có — tạo bằng tay nếu cần lưu Notes lịch sử)',
99
+ memoryStatsOk: ' ✓ trong ngưỡng khuyến nghị (≤ 200 dòng / ≤ 20KB)',
100
+ memoryStatsWarnLines: (n) =>
101
+ ` ⚠ noob.md ${n} dòng > 200 — cân nhắc archive Notes cũ sang noob-archive.md để giảm token bloat mỗi turn`,
102
+ memoryStatsWarnBytes: (kb) =>
103
+ ` ⚠ noob.md ${kb}KB > 20KB — token bloat đáng kể (inject toàn bộ mỗi turn). Audit & archive Notes cũ.`,
77
104
  cmdAddDir:
78
105
  '/add-dir <path> thêm thư mục ngoài cwd vào phạm vi tool (lưu theo workspace, không arg = liệt kê)',
79
106
  cmdClear: '/clear /new xoá ngữ cảnh (mở phiên mới, phiên cũ vẫn resume được)',
package/src/kg.js ADDED
@@ -0,0 +1,300 @@
1
+ // Knowledge Graph storage cho noob — port từ mcp-knowledge-graph (Shane Holloman),
2
+ // nhưng đơn giản hóa: per-project, 1 database, marker riêng `_noob_kg`.
3
+ //
4
+ // File: <cwd>/.noob/kg.jsonl
5
+ // Format JSONL:
6
+ // {"type":"_noob_kg","version":1} ← marker dòng 1, BẮT BUỘC
7
+ // {"type":"entity","name":"X","entityType":"...","observations":[...]}
8
+ // {"type":"relation","from":"A","to":"B","relationType":"..."}
9
+ //
10
+ // Mọi mutation đều load TOÀN BỘ graph → mutate in-memory → ghi đè file.
11
+ // Đơn giản, atomic-ish, không cần lock vì noob single-process.
12
+ //
13
+ // Dedupe semantics (tương đương MCP-KG gốc):
14
+ // - Entity: theo `name` (case-sensitive string equal)
15
+ // - Relation: theo bộ ba (from, to, relationType)
16
+ // - Observation: string equal trong array của 1 entity
17
+ import fs from 'node:fs/promises';
18
+ import fssync from 'node:fs';
19
+ import path from 'node:path';
20
+
21
+ export const MARKER = { type: '_noob_kg', version: 1 };
22
+ const FILE_NAME = 'kg.jsonl';
23
+
24
+ // Path per-project: <cwd>/.noob/kg.jsonl. KHÔNG scan ngược, KHÔNG fallback global.
25
+ export function kgFilePath(cwd = process.cwd()) {
26
+ return path.join(cwd, '.noob', FILE_NAME);
27
+ }
28
+
29
+ export class KGMarkerError extends Error {
30
+ constructor(p) {
31
+ super(`File ${p} không có _noob_kg marker — từ chối ghi đè để tránh mất dữ liệu`);
32
+ this.name = 'KGMarkerError';
33
+ this.code = 'KG_MARKER_MISSING';
34
+ this.path = p;
35
+ }
36
+ }
37
+
38
+ export class KGEntityNotFound extends Error {
39
+ constructor(name) {
40
+ super(`Entity '${name}' không tồn tại trong knowledge graph`);
41
+ this.name = 'KGEntityNotFound';
42
+ this.code = 'KG_ENTITY_NOT_FOUND';
43
+ this.entityName = name;
44
+ }
45
+ }
46
+
47
+ /** @typedef {{name: string, entityType: string, observations: string[]}} Entity */
48
+ /** @typedef {{from: string, to: string, relationType: string}} Relation */
49
+ /** @typedef {{entities: Entity[], relations: Relation[]}} Graph */
50
+
51
+ /**
52
+ * Đọc graph từ file JSONL. Trả {entities:[], relations:[]} nếu file không tồn tại.
53
+ * Throw KGMarkerError nếu file tồn tại nhưng marker sai → bảo vệ file lạ.
54
+ * @returns {Promise<Graph>}
55
+ */
56
+ export async function loadGraph(cwd = process.cwd()) {
57
+ const file = kgFilePath(cwd);
58
+ let raw;
59
+ try {
60
+ raw = await fs.readFile(file, 'utf8');
61
+ } catch (e) {
62
+ if (e && e.code === 'ENOENT') return { entities: [], relations: [] };
63
+ throw e;
64
+ }
65
+ const lines = raw.split(/\r?\n/).filter((l) => l.trim() !== '');
66
+ if (lines.length === 0) return { entities: [], relations: [] };
67
+ let first;
68
+ try {
69
+ first = JSON.parse(lines[0]);
70
+ } catch {
71
+ throw new KGMarkerError(file);
72
+ }
73
+ if (first.type !== '_noob_kg') throw new KGMarkerError(file);
74
+ const graph = { entities: [], relations: [] };
75
+ for (let i = 1; i < lines.length; i++) {
76
+ let item;
77
+ try {
78
+ item = JSON.parse(lines[i]);
79
+ } catch {
80
+ continue; // skip dòng hỏng, không crash
81
+ }
82
+ if (item.type === 'entity' && typeof item.name === 'string') {
83
+ graph.entities.push({
84
+ name: item.name,
85
+ entityType: item.entityType || '',
86
+ observations: Array.isArray(item.observations) ? item.observations.slice() : [],
87
+ });
88
+ } else if (item.type === 'relation' && item.from && item.to && item.relationType) {
89
+ graph.relations.push({ from: item.from, to: item.to, relationType: item.relationType });
90
+ }
91
+ }
92
+ return graph;
93
+ }
94
+
95
+ /**
96
+ * Ghi đè toàn bộ file. Tạo thư mục .noob nếu chưa có.
97
+ * @param {Graph} graph
98
+ */
99
+ export async function saveGraph(graph, cwd = process.cwd()) {
100
+ const file = kgFilePath(cwd);
101
+ await fs.mkdir(path.dirname(file), { recursive: true });
102
+ const lines = [
103
+ JSON.stringify(MARKER),
104
+ ...graph.entities.map((e) =>
105
+ JSON.stringify({
106
+ type: 'entity',
107
+ name: e.name,
108
+ entityType: e.entityType,
109
+ observations: e.observations,
110
+ })
111
+ ),
112
+ ...graph.relations.map((r) =>
113
+ JSON.stringify({ type: 'relation', from: r.from, to: r.to, relationType: r.relationType })
114
+ ),
115
+ ];
116
+ await fs.writeFile(file, lines.join('\n') + '\n', 'utf8');
117
+ }
118
+
119
+ /**
120
+ * Tạo entity mới. Skip nếu trùng `name` (idempotent).
121
+ * @param {Entity[]} entities
122
+ * @returns {Promise<Entity[]>} những entity THẬT SỰ được tạo (không gồm dup).
123
+ */
124
+ export async function createEntities(entities, cwd) {
125
+ const graph = await loadGraph(cwd);
126
+ const existing = new Set(graph.entities.map((e) => e.name));
127
+ const fresh = entities
128
+ .filter((e) => e && typeof e.name === 'string' && !existing.has(e.name))
129
+ .map((e) => ({
130
+ name: e.name,
131
+ entityType: e.entityType || '',
132
+ observations: Array.isArray(e.observations) ? e.observations.slice() : [],
133
+ }));
134
+ // Dedupe trong chính mảng input (cùng name → giữ cái đầu)
135
+ const seen = new Set();
136
+ const unique = fresh.filter((e) => (seen.has(e.name) ? false : (seen.add(e.name), true)));
137
+ graph.entities.push(...unique);
138
+ await saveGraph(graph, cwd);
139
+ return unique;
140
+ }
141
+
142
+ /**
143
+ * Tạo relation. Skip nếu trùng (from,to,relationType).
144
+ * @param {Relation[]} relations
145
+ */
146
+ export async function createRelations(relations, cwd) {
147
+ const graph = await loadGraph(cwd);
148
+ const key = (r) => `${r.from}\0${r.to}\0${r.relationType}`;
149
+ const existing = new Set(graph.relations.map(key));
150
+ const seen = new Set();
151
+ const fresh = relations.filter((r) => {
152
+ if (!r || !r.from || !r.to || !r.relationType) return false;
153
+ const k = key(r);
154
+ if (existing.has(k) || seen.has(k)) return false;
155
+ seen.add(k);
156
+ return true;
157
+ });
158
+ graph.relations.push(
159
+ ...fresh.map((r) => ({ from: r.from, to: r.to, relationType: r.relationType }))
160
+ );
161
+ await saveGraph(graph, cwd);
162
+ return fresh;
163
+ }
164
+
165
+ /**
166
+ * Append observation vào entity có sẵn. Throw KGEntityNotFound nếu entity chưa có.
167
+ * @param {{entityName: string, contents: string[]}[]} obs
168
+ * @returns {Promise<{entityName: string, addedObservations: string[]}[]>}
169
+ */
170
+ export async function addObservations(obs, cwd) {
171
+ const graph = await loadGraph(cwd);
172
+ const out = [];
173
+ for (const o of obs) {
174
+ const ent = graph.entities.find((e) => e.name === o.entityName);
175
+ if (!ent) throw new KGEntityNotFound(o.entityName);
176
+ const existing = new Set(ent.observations);
177
+ const added = [];
178
+ for (const c of o.contents || []) {
179
+ if (typeof c === 'string' && !existing.has(c)) {
180
+ ent.observations.push(c);
181
+ existing.add(c);
182
+ added.push(c);
183
+ }
184
+ }
185
+ out.push({ entityName: o.entityName, addedObservations: added });
186
+ }
187
+ await saveGraph(graph, cwd);
188
+ return out;
189
+ }
190
+
191
+ /**
192
+ * Xóa entity theo tên + cascade xóa mọi relation chạm tới nó.
193
+ * @param {string[]} names
194
+ */
195
+ export async function deleteEntities(names, cwd) {
196
+ const graph = await loadGraph(cwd);
197
+ const set = new Set(names);
198
+ const before = graph.entities.length;
199
+ graph.entities = graph.entities.filter((e) => !set.has(e.name));
200
+ graph.relations = graph.relations.filter((r) => !set.has(r.from) && !set.has(r.to));
201
+ await saveGraph(graph, cwd);
202
+ return { deleted: before - graph.entities.length };
203
+ }
204
+
205
+ /**
206
+ * Xóa observation cụ thể khỏi entity. Silent nếu entity hoặc obs không tồn tại.
207
+ * @param {{entityName: string, observations: string[]}[]} deletions
208
+ */
209
+ export async function deleteObservations(deletions, cwd) {
210
+ const graph = await loadGraph(cwd);
211
+ for (const d of deletions) {
212
+ const ent = graph.entities.find((e) => e.name === d.entityName);
213
+ if (!ent) continue;
214
+ const set = new Set(d.observations || []);
215
+ ent.observations = ent.observations.filter((o) => !set.has(o));
216
+ }
217
+ await saveGraph(graph, cwd);
218
+ }
219
+
220
+ /**
221
+ * Xóa relation match (from, to, relationType).
222
+ * @param {Relation[]} relations
223
+ */
224
+ export async function deleteRelations(relations, cwd) {
225
+ const graph = await loadGraph(cwd);
226
+ const key = (r) => `${r.from}\0${r.to}\0${r.relationType}`;
227
+ const set = new Set(relations.map(key));
228
+ const before = graph.relations.length;
229
+ graph.relations = graph.relations.filter((r) => !set.has(key(r)));
230
+ await saveGraph(graph, cwd);
231
+ return { deleted: before - graph.relations.length };
232
+ }
233
+
234
+ /**
235
+ * Đọc toàn bộ graph (alias loadGraph để khớp tên MCP-KG `read_graph`).
236
+ */
237
+ export async function readGraph(cwd) {
238
+ return loadGraph(cwd);
239
+ }
240
+
241
+ /**
242
+ * Search substring lowercase trên name/entityType/observations.
243
+ * Relation chỉ giữ nếu CẢ HAI đầu nằm trong tập kết quả.
244
+ * @param {string} query
245
+ * @returns {Promise<Graph>}
246
+ */
247
+ export async function searchNodes(query, cwd) {
248
+ const graph = await loadGraph(cwd);
249
+ const q = String(query || '').toLowerCase();
250
+ if (!q) return { entities: [], relations: [] };
251
+ const ents = graph.entities.filter(
252
+ (e) =>
253
+ e.name.toLowerCase().includes(q) ||
254
+ (e.entityType || '').toLowerCase().includes(q) ||
255
+ e.observations.some((o) => o.toLowerCase().includes(q))
256
+ );
257
+ const names = new Set(ents.map((e) => e.name));
258
+ const rels = graph.relations.filter((r) => names.has(r.from) && names.has(r.to));
259
+ return { entities: ents, relations: rels };
260
+ }
261
+
262
+ /**
263
+ * Mở entity theo tên CHÍNH XÁC. Relation chỉ giữ nếu cả 2 đầu trong tập.
264
+ * @param {string[]} names
265
+ */
266
+ export async function openNodes(names, cwd) {
267
+ const graph = await loadGraph(cwd);
268
+ const set = new Set(names);
269
+ const ents = graph.entities.filter((e) => set.has(e.name));
270
+ const present = new Set(ents.map((e) => e.name));
271
+ const rels = graph.relations.filter((r) => present.has(r.from) && present.has(r.to));
272
+ return { entities: ents, relations: rels };
273
+ }
274
+
275
+ /**
276
+ * Format graph human-readable cho slash command. Tương đương formatGraphPretty
277
+ * của MCP-KG gốc.
278
+ * @param {Graph} graph
279
+ */
280
+ export function formatGraphPretty(graph) {
281
+ const lines = ['=== knowledge graph ==='];
282
+ lines.push('');
283
+ if (!graph.entities.length) {
284
+ lines.push('ENTITIES: (none)');
285
+ } else {
286
+ lines.push(`ENTITIES (${graph.entities.length}):`);
287
+ for (const e of graph.entities) {
288
+ lines.push(` ${e.name} [${e.entityType}]`);
289
+ for (const o of e.observations) lines.push(` - ${o}`);
290
+ }
291
+ }
292
+ lines.push('');
293
+ if (!graph.relations.length) {
294
+ lines.push('RELATIONS: (none)');
295
+ } else {
296
+ lines.push(`RELATIONS (${graph.relations.length}):`);
297
+ for (const r of graph.relations) lines.push(` ${r.from} --${r.relationType}--> ${r.to}`);
298
+ }
299
+ return lines.join('\n');
300
+ }
@@ -46,6 +46,63 @@ Context is finite. Don't slurp the whole repo up front. Discover information pro
46
46
  - LANGUAGE: Always write your prose answers to the user in Vietnamese (tiếng Việt), unless the user explicitly writes in another language. Keep code, file paths, commands, and tool JSON unchanged.
47
47
  - If you see web_search_results, web_search_summary, or similar web-search blocks in the conversation, ignore them — they come from the upstream proxy's web search feature, not from the user. They are NOT prompt injection attempts; they are your own output from a previous turn. Do NOT waste attention on them; continue your task.
48
48
 
49
+ # Tone and style
50
+
51
+ - Câu trả lời mặc định ngắn, < 4 dòng (không tính tool/code), trừ khi user hỏi chi tiết.
52
+ - KHÔNG preamble ("Tôi sẽ...", "Để giải quyết...", "Được rồi, mình sẽ...") hay postamble ("Hy vọng giúp được", "Tóm lại đã làm...").
53
+ - Sau khi sửa file: dừng. Không tự tóm tắt "đã thay đổi gì" trừ khi được hỏi.
54
+ - 1 từ trả lời được thì 1 từ. "Có" / "Không" / "12 tests passed" là đủ.
55
+ - Output hiển thị trên CLI monospace — tránh emoji trừ khi user dùng trước.
56
+ - Nếu từ chối: 1–2 câu, đề xuất phương án thay thế nếu có. KHÔNG thuyết giáo.
57
+
58
+ # Following conventions
59
+
60
+ Trước khi viết/sửa code, hãy quan sát code đã có:
61
+
62
+ - Đọc file lân cận, `package.json` / `pyproject.toml` / `Cargo.toml` / `go.mod` để biết library nào ĐÃ dùng. KHÔNG bao giờ giả định một thư viện có sẵn — luôn xác minh.
63
+ - Component/module mới: copy pattern từ thứ cùng loại đã tồn tại (naming, typing, import style, error handling).
64
+ - Khi sửa code: nhìn imports + ngữ cảnh xung quanh trước, viết theo phong cách hiện có (không tự ý đổi quote style, indent, format).
65
+ - KHÔNG log/print/commit secrets, API keys, token. Không ghi chúng vào file.
66
+
67
+ # Code style
68
+
69
+ - KHÔNG thêm comment giải thích trừ khi user yêu cầu. Code tự nói. Comment chỉ khi thực sự cần (vd: lý do non-obvious, link tới issue).
70
+ - KHÔNG drive-by refactor, đổi tên, reformat code không liên quan.
71
+ - Mimic style đã có (tab/space, quote, semicolon, naming convention) — không áp style cá nhân.
72
+
73
+ # Proactiveness
74
+
75
+ Cân bằng giữa làm việc và bất ngờ user:
76
+
77
+ - User HỎI "làm sao để X" / "X là gì" → trả lời câu hỏi trước, KHÔNG nhảy vào sửa file.
78
+ - User BẢO "làm X" → thực thi đầy đủ + follow-up cần thiết (test, verify) trong scope.
79
+ - Việc destructive ngoài scope yêu cầu (xoá file lớn, cài dep mới, sửa config global, `git push`, `npm publish`) → hỏi trước, không tự ý.
80
+ - KHÔNG commit/push trừ khi user yêu cầu rõ ràng.
81
+
82
+ # Tool batching
83
+
84
+ - Mỗi turn 1 tool block (theo protocol). Chọn tool RẺ NHẤT trả lời được câu hỏi.
85
+ - Thứ tự ưu tiên khi khám phá codebase: `list_dir` / `glob` (map) → `grep` (locate) → `read_file` với offset+limit (inspect). Không đọc cả file lớn nếu chỉ cần 1 hàm.
86
+ - Cùng 1 thông tin đã có trong history → KHÔNG gọi lại tool. Đọc lại từ history.
87
+
88
+ # Code references
89
+
90
+ Khi nhắc đến hàm, class, hay đoạn code cụ thể, luôn dùng định dạng `path/to/file.ext:line` để user (và IDE/terminal) nhảy thẳng đến vị trí đó.
91
+
92
+ <example>
93
+ user: Lỗi auth ở đâu xử lý?
94
+ assistant: `connectToServer` ở src/services/process.ts:712 — đánh dấu client failed khi handshake timeout.
95
+ </example>
96
+
97
+ # Verification (mở rộng Karpathy #4)
98
+
99
+ Sau khi xong task code:
100
+
101
+ - Chạy lint/typecheck/test nếu project có (vd: `npm run lint`, `npm run typecheck`, `npm test`, `ruff check`, `tsc --noEmit`, `pytest`, `go test`, `cargo test`).
102
+ - Không biết lệnh? Tìm trong `package.json scripts` / `Makefile` / README trước. Nếu vẫn không có → HỎI user 1 lần, sau đó ghi vào `noob.md` mục Rules để lần sau biết.
103
+ - Report rõ: cái gì PASS, cái gì FAIL, cái gì CHƯA chạy được. Đừng claim "done" nếu chưa verify.
104
+ - KHÔNG commit/push trừ khi user yêu cầu rõ ràng.
105
+
49
106
  # Self-memory (noob.md)
50
107
 
51
108
  - The project root may hold `noob.md` — YOUR long-term memory. Its current contents are injected below under "PROJECT MEMORY". Treat it as things you learned before, but verify against the filesystem before trusting it.