@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 +9 -0
- package/package.json +1 -1
- package/src/agent.js +5 -0
- package/src/api.js +48 -0
- package/src/i18n.js +28 -1
- package/src/kg.js +300 -0
- package/src/prompts/system.md +57 -0
- package/src/repl/agent-dispatch.js +168 -0
- package/src/repl/permission.js +23 -11
- package/src/repl/state.js +3 -1
- package/src/repl.js +256 -137
- package/src/sessions.js +89 -0
- package/src/skills.js +59 -0
- package/src/tools.js +56 -0
- package/src/tui.js +21 -3
- package/src/ui.js +86 -84
package/src/repl.js
CHANGED
|
@@ -4,9 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { createTui } from './tui.js';
|
|
6
6
|
import { runAgent, maybeSummarize, buildSystem, buildUserMessage } from './agent.js';
|
|
7
|
-
import {
|
|
7
|
+
import { spawnAgentToolsDoc, MAX_SUBAGENT_DEPTH } from './subagent.js';
|
|
8
8
|
import { TokenMeter, countMessages, CONTEXT_WINDOW, countTokens } from './tokens.js';
|
|
9
|
-
import { stream, usage, cachedUsage, resetUsageCache, ApiError, resetMemoryToken } from './api.js';
|
|
9
|
+
import { stream, usage, cachedUsage, resetUsageCache, evaluateQuotaWarning, ApiError, resetMemoryToken } from './api.js';
|
|
10
10
|
import {
|
|
11
11
|
runTool,
|
|
12
12
|
describe,
|
|
@@ -35,17 +35,27 @@ import { t } from './i18n.js';
|
|
|
35
35
|
import { checkLatest, runUpdate, CURRENT } from './update.js';
|
|
36
36
|
import * as sessions from './sessions.js';
|
|
37
37
|
import { loadSkill, listSkills } from './skills.js';
|
|
38
|
+
import {
|
|
39
|
+
loadGraph as kgLoad,
|
|
40
|
+
createEntities as kgCreateEntities,
|
|
41
|
+
createRelations as kgCreateRelations,
|
|
42
|
+
addObservations as kgAddObservations,
|
|
43
|
+
deleteEntities as kgDeleteEntities,
|
|
44
|
+
deleteObservations as kgDeleteObservations,
|
|
45
|
+
deleteRelations as kgDeleteRelations,
|
|
46
|
+
searchNodes as kgSearchNodes,
|
|
47
|
+
openNodes as kgOpenNodes,
|
|
48
|
+
formatGraphPretty as kgFormat,
|
|
49
|
+
kgFilePath,
|
|
50
|
+
KGEntityNotFound,
|
|
51
|
+
KGMarkerError,
|
|
52
|
+
} from './kg.js';
|
|
38
53
|
import { saveWorkflow, loadWorkflow } from './workflows.js';
|
|
39
54
|
import {
|
|
40
55
|
createRun as createWorkflowRun,
|
|
41
56
|
loadRun as loadWorkflowRun,
|
|
42
57
|
listRuns as listWorkflowRuns,
|
|
43
58
|
closeRun as closeWorkflowRun,
|
|
44
|
-
hashTask as hashWorkflowTask,
|
|
45
|
-
lookupTaskResult as lookupWorkflowTaskResult,
|
|
46
|
-
recordTaskStart as recordWorkflowTaskStart,
|
|
47
|
-
recordTaskDone as recordWorkflowTaskDone,
|
|
48
|
-
recordTaskFailed as recordWorkflowTaskFailed,
|
|
49
59
|
} from './workflow-runs.js';
|
|
50
60
|
import { getBuiltinWorkflow, loadBuiltinPrompt } from './workflows-builtin.js';
|
|
51
61
|
import { SLASH, completeInput, mentionedFiles } from './repl/complete.js';
|
|
@@ -70,6 +80,7 @@ import { createState } from './repl/state.js';
|
|
|
70
80
|
import {
|
|
71
81
|
shortCwd, shortPath, relTime, firstLine, truncate, fmtTime, fmtK, preview,
|
|
72
82
|
} from './repl/utils.js';
|
|
83
|
+
import { createAgentDispatcher } from './repl/agent-dispatch.js';
|
|
73
84
|
export async function startRepl(opts = {}) {
|
|
74
85
|
const state = createState(opts, config);
|
|
75
86
|
const tokenMeter = new TokenMeter();
|
|
@@ -148,13 +159,43 @@ export async function startRepl(opts = {}) {
|
|
|
148
159
|
// ĐỪNG DI CHUYỂN khối này lên trên: dependencies cần sẵn — `tui` (~line 112),
|
|
149
160
|
// `pending` (~line 107), `ask` (~line 136), `c`/`t`/`truncate` (import top).
|
|
150
161
|
// Nếu dời lên trước `ask` sẽ lại TDZ. Vị trí hiện tại là điểm sớm nhất hợp lệ.
|
|
151
|
-
const askPermission = (name) =>
|
|
152
|
-
_askPermission(name, { tui, ask, pending, c, t, truncate });
|
|
162
|
+
const askPermission = (name, targetPath) =>
|
|
163
|
+
_askPermission(name, { tui, ask, pending, c, t, truncate, targetPath });
|
|
153
164
|
const askAddRoot = (root, targetPath) =>
|
|
154
165
|
_askAddRoot(root, targetPath, { tui, ask, pending, c, t, truncate });
|
|
155
166
|
const askWorkflowAgentMode = () =>
|
|
156
167
|
_askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
|
|
157
168
|
|
|
169
|
+
// ── quota soft-cap guard ───────────────────────────────────────────
|
|
170
|
+
// Trước khi start ULTRA / loop / workflow (3 lệnh ăn nhiều request nhất),
|
|
171
|
+
// check quota cached. Nếu sắp cạn → cảnh báo + yêu cầu confirm y. Mục đích:
|
|
172
|
+
// chống user tự đốt key vô ý, đặc biệt key trial 200 req hết là dead.
|
|
173
|
+
//
|
|
174
|
+
// Trả về true (cho phép tiếp) / false (huỷ). Trong --yolo vẫn IN cảnh báo
|
|
175
|
+
// nhưng skip confirm (giữ visibility).
|
|
176
|
+
async function checkQuotaBeforeHeavy(label) {
|
|
177
|
+
let u = cachedUsage();
|
|
178
|
+
if (!u) {
|
|
179
|
+
// Chưa có cache → fetch 1 lần (cache 90s ở api.js, không gọi lại nhiều).
|
|
180
|
+
try {
|
|
181
|
+
u = await usage();
|
|
182
|
+
} catch {
|
|
183
|
+
return true; // không lấy được usage → đừng chặn user.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const warn = evaluateQuotaWarning(u, label, t);
|
|
187
|
+
if (!warn) return true;
|
|
188
|
+
console.log(c.err(' ⚠ ' + t.quotaWarnTitle + ': ' + warn.message));
|
|
189
|
+
if (state.yolo) {
|
|
190
|
+
console.log(c.dim(' ' + t.quotaYoloBypass));
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
const ans = ((await ask(c.tool(' ' + t.quotaConfirm + ' '))) ?? '').trim().toLowerCase();
|
|
194
|
+
if (ans === 'y' || ans === 'yes' || ans === 'có') return true;
|
|
195
|
+
console.log(c.dim(' ' + t.quotaCancelled(label)));
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
158
199
|
// NOOB_DEBUG=1: vạch trần đường thoát thật sự (xác nhận/loại bỏ giả thuyết
|
|
159
200
|
// "event loop cạn"). 'beforeExit' nổ = loop cạn (stdin chết). 'exit' = thoát.
|
|
160
201
|
if (process.env.NOOB_DEBUG === '1') {
|
|
@@ -420,6 +461,8 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
|
|
|
420
461
|
// `builtInName` (optional): nếu có thì SKIP loadSkill dynamic-workflows (prompt
|
|
421
462
|
// built-in đã hardcode pattern + step cụ thể rồi, không cần model design lại).
|
|
422
463
|
async function workflowExecute(userRequest, { builtInName = null, resumeRun = null } = {}) {
|
|
464
|
+
// Resume KHÔNG cần check (đang tiếp tục run dở, user đã chấp nhận trước đó).
|
|
465
|
+
if (!resumeRun && !(await checkQuotaBeforeHeavy('/workflow'))) return;
|
|
423
466
|
let prompt;
|
|
424
467
|
if (builtInName) {
|
|
425
468
|
// Built-in prompt đã đầy đủ PLAN + 4 bước thực thi — KHÔNG wrap thêm skill.
|
|
@@ -452,7 +495,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
452
495
|
2. Spawn sub-agent theo plan — mỗi prompt sub-agent có 5 mục GOAL/INPUTS/METHOD/OUTPUT SHAPE/STOP CONDITION.
|
|
453
496
|
3. Gom kết quả, dedupe, reconcile mâu thuẫn, viết báo cáo cuối tiếng Việt. Sub-agent KHÔNG nói trực tiếp với user.`;
|
|
454
497
|
}
|
|
455
|
-
if (!state.
|
|
498
|
+
if (!state.agentMode) {
|
|
456
499
|
// Đừng tự bật — workflow cần spawn_agent, đây là quyền nặng (sub-agent chạy
|
|
457
500
|
// tool độc lập). Hỏi 1 lần, user chọn y thì bật & chạy, n thì huỷ sạch +
|
|
458
501
|
// gợi ý /agent. Tránh buộc user gõ lại /workflow sau khi /agent.
|
|
@@ -466,7 +509,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
466
509
|
)
|
|
467
510
|
);
|
|
468
511
|
}
|
|
469
|
-
state.
|
|
512
|
+
state.agentMode = true;
|
|
470
513
|
console.log(
|
|
471
514
|
c.tool(' ✓ ' + (t.workflowAgentEnabled || 'đã bật agent mode cho workflow này.'))
|
|
472
515
|
);
|
|
@@ -794,6 +837,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
794
837
|
async function runUltra(goal) {
|
|
795
838
|
if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
|
|
796
839
|
if (!goal) return console.log(c.err(' ' + t.ultraNeedGoal));
|
|
840
|
+
if (!(await checkQuotaBeforeHeavy('/ultra'))) return;
|
|
797
841
|
state.mode = 'chat'; // tự hành chỉ chạy ở chế độ agent
|
|
798
842
|
state.ultra = true;
|
|
799
843
|
console.log(c.accent(' 🚀 ' + t.ultraOn));
|
|
@@ -966,6 +1010,7 @@ Sau khi xong tick này, TỰ ĐÁNH GIÁ:
|
|
|
966
1010
|
if (!task) return console.log(c.err(' ' + t.loopNeedArgs));
|
|
967
1011
|
const intervalMs = parseInterval(intervalStr);
|
|
968
1012
|
if (!intervalMs) return console.log(c.err(' ' + t.loopBadInterval(intervalStr)));
|
|
1013
|
+
if (!(await checkQuotaBeforeHeavy('/loop'))) return;
|
|
969
1014
|
const normInterval = fmtMs(intervalMs);
|
|
970
1015
|
state.loop = {
|
|
971
1016
|
intervalMs,
|
|
@@ -1102,6 +1147,50 @@ NGUYÊN TẮC:
|
|
|
1102
1147
|
console.log(c.dim(' ' + memoryPath() + t.memoryStat(mem.split('\n').length)));
|
|
1103
1148
|
}
|
|
1104
1149
|
|
|
1150
|
+
// /memory stats — thống kê size noob.md + noob-archive.md, cảnh báo bloat.
|
|
1151
|
+
// Mỗi turn agent inject toàn bộ noob.md vào prompt → file dài = token bloat.
|
|
1152
|
+
// Ngưỡng khuyến nghị: ≤ 200 dòng / ≤ 20KB (~5k token cl100k_base).
|
|
1153
|
+
function runMemoryStats() {
|
|
1154
|
+
const fmtBytes = (n) => (n >= 1024 ? (n / 1024).toFixed(1) + 'KB' : n + 'B');
|
|
1155
|
+
const mp = memoryPath();
|
|
1156
|
+
const ap = path.resolve(process.cwd(), 'noob-archive.md');
|
|
1157
|
+
console.log(c.tool(' ' + t.memoryStatsHeader));
|
|
1158
|
+
let mainLines = 0;
|
|
1159
|
+
let mainBytes = 0;
|
|
1160
|
+
let mainKB = 0;
|
|
1161
|
+
try {
|
|
1162
|
+
const st = fs.statSync(mp);
|
|
1163
|
+
mainBytes = st.size;
|
|
1164
|
+
mainKB = mainBytes / 1024;
|
|
1165
|
+
const txt = fs.readFileSync(mp, 'utf8');
|
|
1166
|
+
mainLines = txt.split('\n').length;
|
|
1167
|
+
const stats = memoryStats() || { rules: 0, notes: 0, mtime: st.mtimeMs };
|
|
1168
|
+
console.log(
|
|
1169
|
+
c.dim(
|
|
1170
|
+
t.memoryStatsMain(mainLines, fmtBytes(mainBytes), stats.rules, stats.notes, relTime(stats.mtime))
|
|
1171
|
+
)
|
|
1172
|
+
);
|
|
1173
|
+
} catch {
|
|
1174
|
+
console.log(c.dim(' noob.md : (chưa có — gõ /init để tạo)'));
|
|
1175
|
+
}
|
|
1176
|
+
try {
|
|
1177
|
+
const st = fs.statSync(ap);
|
|
1178
|
+
const txt = fs.readFileSync(ap, 'utf8');
|
|
1179
|
+
const lines = txt.split('\n').length;
|
|
1180
|
+
console.log(c.dim(t.memoryStatsArchive(lines, fmtBytes(st.size), relTime(st.mtimeMs))));
|
|
1181
|
+
} catch {
|
|
1182
|
+
console.log(c.dim(t.memoryStatsArchiveMissing));
|
|
1183
|
+
}
|
|
1184
|
+
const warnLines = mainLines > 200;
|
|
1185
|
+
const warnBytes = mainKB > 20;
|
|
1186
|
+
if (!warnLines && !warnBytes) {
|
|
1187
|
+
if (mainLines > 0) console.log(c.ok(' ' + t.memoryStatsOk));
|
|
1188
|
+
} else {
|
|
1189
|
+
if (warnLines) console.log(c.err(' ' + t.memoryStatsWarnLines(mainLines)));
|
|
1190
|
+
if (warnBytes) console.log(c.err(' ' + t.memoryStatsWarnBytes(mainKB.toFixed(1))));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1105
1194
|
// /auto-yolo — lưu/bỏ yolo làm MẶC ĐỊNH (mỗi lần mở noob tự bật). Vì yolo tự
|
|
1106
1195
|
// duyệt mọi thao tác, BẬT phải xác nhận lần 2 bằng cách gõ 'y'. TẮT thì không.
|
|
1107
1196
|
async function toggleAutoYolo() {
|
|
@@ -1324,126 +1413,13 @@ NGUYÊN TẮC:
|
|
|
1324
1413
|
startSpin(t.thinking);
|
|
1325
1414
|
let printer = null;
|
|
1326
1415
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
result: 'ERROR: agent mode đang TẮT — gõ /agent on để bật trước khi spawn.',
|
|
1335
|
-
};
|
|
1336
|
-
if (depth >= MAX_SUBAGENT_DEPTH)
|
|
1337
|
-
return {
|
|
1338
|
-
allow: true,
|
|
1339
|
-
result: `ERROR: đã đạt depth tối đa (${MAX_SUBAGENT_DEPTH}) — không spawn thêm.`,
|
|
1340
|
-
};
|
|
1341
|
-
const tasks =
|
|
1342
|
-
name === 'spawn_agent' ? [input] : Array.isArray(input?.agents) ? input.agents : [];
|
|
1343
|
-
if (!tasks.length) return { allow: true, result: 'ERROR: thiếu task cho sub-agent.' };
|
|
1344
|
-
stopSpin();
|
|
1345
|
-
console.log(
|
|
1346
|
-
chalk.hex('#8b5cf6')(
|
|
1347
|
-
` ⊕ spawn ${tasks.length} sub-agent (depth ${depth + 1}/${MAX_SUBAGENT_DEPTH})`
|
|
1348
|
-
)
|
|
1349
|
-
);
|
|
1350
|
-
startSpin(t.thinking);
|
|
1351
|
-
try {
|
|
1352
|
-
const runData = state.workflowRun?.data || null;
|
|
1353
|
-
const results = await Promise.all(
|
|
1354
|
-
tasks.map((task, i) => {
|
|
1355
|
-
// Per-sub-agent model routing: task.model có thể là id model hoặc tên thân thiện.
|
|
1356
|
-
// findModel() resolve cả hai; nếu không match thì fallback model của cha.
|
|
1357
|
-
let subModel = state.model.id;
|
|
1358
|
-
let modelTag = '';
|
|
1359
|
-
if (task?.model) {
|
|
1360
|
-
const m = findModel(task.model);
|
|
1361
|
-
if (m) {
|
|
1362
|
-
subModel = m.id;
|
|
1363
|
-
modelTag = ` [${m.name}]`;
|
|
1364
|
-
} else
|
|
1365
|
-
modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
|
|
1366
|
-
}
|
|
1367
|
-
const taskBody = task?.task || task?.prompt || '';
|
|
1368
|
-
const taskCtx = task?.context || '';
|
|
1369
|
-
// Workflow journal: nếu đang trong run + task đã done lần trước → return
|
|
1370
|
-
// cached result, tiết kiệm token. Hash = crc32(task+ctx+model).
|
|
1371
|
-
if (runData) {
|
|
1372
|
-
const hash = hashWorkflowTask({ task: taskBody, context: taskCtx, model: subModel });
|
|
1373
|
-
const cached = lookupWorkflowTaskResult(runData, hash);
|
|
1374
|
-
if (cached !== null) {
|
|
1375
|
-
stopSpin();
|
|
1376
|
-
console.log(
|
|
1377
|
-
chalk.hex('#8b5cf6')(
|
|
1378
|
-
` ⊘ sub-agent #${i + 1}${modelTag} skip — đã done trong run trước (cached)`
|
|
1379
|
-
)
|
|
1380
|
-
);
|
|
1381
|
-
startSpin(t.thinking);
|
|
1382
|
-
return Promise.resolve(
|
|
1383
|
-
`── sub-agent #${i + 1}${modelTag} (cached) ──\n${cached}`
|
|
1384
|
-
);
|
|
1385
|
-
}
|
|
1386
|
-
recordWorkflowTaskStart(runData, {
|
|
1387
|
-
hash,
|
|
1388
|
-
task: taskBody,
|
|
1389
|
-
context: taskCtx,
|
|
1390
|
-
model: subModel,
|
|
1391
|
-
});
|
|
1392
|
-
// [GỠ BUDGET 2026-06-06] Sub-agent chạy không giới hạn token.
|
|
1393
|
-
return runSubAgent({
|
|
1394
|
-
task: taskBody,
|
|
1395
|
-
context: taskCtx,
|
|
1396
|
-
depth: depth + 1,
|
|
1397
|
-
model: subModel,
|
|
1398
|
-
signal: abort.signal,
|
|
1399
|
-
tokenMeter,
|
|
1400
|
-
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
1401
|
-
onLog: (msg) => {
|
|
1402
|
-
stopSpin();
|
|
1403
|
-
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
1404
|
-
startSpin(t.thinking);
|
|
1405
|
-
},
|
|
1406
|
-
})
|
|
1407
|
-
.then((r) => {
|
|
1408
|
-
recordWorkflowTaskDone(runData, hash, r);
|
|
1409
|
-
return `── sub-agent #${i + 1}${modelTag} ──\n${r}`;
|
|
1410
|
-
})
|
|
1411
|
-
.catch((e) => {
|
|
1412
|
-
recordWorkflowTaskFailed(runData, hash, e);
|
|
1413
|
-
return `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`;
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
// Không có active workflow run → behavior cũ.
|
|
1417
|
-
return runSubAgent({
|
|
1418
|
-
task: taskBody,
|
|
1419
|
-
context: taskCtx,
|
|
1420
|
-
depth: depth + 1,
|
|
1421
|
-
model: subModel,
|
|
1422
|
-
signal: abort.signal,
|
|
1423
|
-
tokenMeter,
|
|
1424
|
-
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
1425
|
-
onLog: (msg) => {
|
|
1426
|
-
stopSpin();
|
|
1427
|
-
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
1428
|
-
startSpin(t.thinking);
|
|
1429
|
-
},
|
|
1430
|
-
})
|
|
1431
|
-
.then((r) => `── sub-agent #${i + 1}${modelTag} ──\n${r}`)
|
|
1432
|
-
.catch(
|
|
1433
|
-
(e) => `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`
|
|
1434
|
-
);
|
|
1435
|
-
})
|
|
1436
|
-
);
|
|
1437
|
-
return { allow: true, result: results.join('\n\n') };
|
|
1438
|
-
} catch (err) {
|
|
1439
|
-
return { allow: true, result: 'ERROR sub-agent: ' + (err?.message || String(err)) };
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
stopSpin();
|
|
1443
|
-
const res = await execTool(name, input);
|
|
1444
|
-
startSpin(t.thinking);
|
|
1445
|
-
return res;
|
|
1446
|
-
};
|
|
1416
|
+
// dispatchTool: xử lý spawn_agent/spawn_agents (sub-agent + workflow journal)
|
|
1417
|
+
// hoặc forward sang execTool cho các tool thường. Logic tách sang
|
|
1418
|
+
// src/repl/agent-dispatch.js (v1.12.x). Factory được gọi MỖI turn vì abort
|
|
1419
|
+
// được rebind trong handle() — không cache.
|
|
1420
|
+
const dispatchTool = createAgentDispatcher({
|
|
1421
|
+
state, abort, tokenMeter, stopSpin, startSpin, execTool,
|
|
1422
|
+
});
|
|
1447
1423
|
|
|
1448
1424
|
const answer = await runAgent({
|
|
1449
1425
|
history: state.history,
|
|
@@ -1495,6 +1471,9 @@ NGUYÊN TẮC:
|
|
|
1495
1471
|
} finally {
|
|
1496
1472
|
abort = null;
|
|
1497
1473
|
tui.setBusy(false);
|
|
1474
|
+
// Reset turn-scoped auto-approve — chỉ áp dụng trong runAgent vừa rồi.
|
|
1475
|
+
// (autoApprove + autoApproveFile vẫn giữ nguyên cho phiên.)
|
|
1476
|
+
state.autoApproveTurn.clear();
|
|
1498
1477
|
// Auto-compact dựa trên context tokens thay vì chars.
|
|
1499
1478
|
// Với CONTEXT_WINDOW = 2M tokens (xem src/tokens.js):
|
|
1500
1479
|
// 75% (1.5M tokens) → auto compact
|
|
@@ -1604,12 +1583,34 @@ NGUYÊN TẮC:
|
|
|
1604
1583
|
}
|
|
1605
1584
|
}
|
|
1606
1585
|
|
|
1607
|
-
if (DESTRUCTIVE.has(name) && !state.yolo
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1586
|
+
if (DESTRUCTIVE.has(name) && !state.yolo) {
|
|
1587
|
+
// Resolve absolute path nếu tool có .path (edit_file/write_file).
|
|
1588
|
+
// run_command không có path → bỏ qua scope `f`.
|
|
1589
|
+
const rawPath = typeof input?.path === 'string' && input.path.length > 0 ? input.path : null;
|
|
1590
|
+
let absPath = null;
|
|
1591
|
+
if (rawPath) {
|
|
1592
|
+
try { absPath = path.resolve(process.cwd(), rawPath); } catch { absPath = null; }
|
|
1593
|
+
}
|
|
1594
|
+
const fileKey = absPath ? name + ':' + absPath : null;
|
|
1595
|
+
|
|
1596
|
+
const preApproved =
|
|
1597
|
+
state.autoApprove.has(name) ||
|
|
1598
|
+
state.autoApproveTurn.has(name) ||
|
|
1599
|
+
(fileKey && state.autoApproveFile.has(fileKey));
|
|
1600
|
+
|
|
1601
|
+
if (!preApproved) {
|
|
1602
|
+
const a = await askPermission(name, absPath);
|
|
1603
|
+
if (a === 'a') state.autoApprove.add(name);
|
|
1604
|
+
else if (a === 't') {
|
|
1605
|
+
state.autoApproveTurn.add(name);
|
|
1606
|
+
console.log(c.dim(' ' + t.permGrantedTurn(name)));
|
|
1607
|
+
} else if (a === 'f' && fileKey) {
|
|
1608
|
+
state.autoApproveFile.add(fileKey);
|
|
1609
|
+
console.log(c.dim(' ' + t.permGrantedFile(name, rawPath)));
|
|
1610
|
+
} else if (a === 'n') {
|
|
1611
|
+
console.log(c.err(' ' + t.denied));
|
|
1612
|
+
return { allow: false };
|
|
1613
|
+
}
|
|
1613
1614
|
}
|
|
1614
1615
|
}
|
|
1615
1616
|
|
|
@@ -1701,6 +1702,120 @@ NGUYÊN TẮC:
|
|
|
1701
1702
|
);
|
|
1702
1703
|
break;
|
|
1703
1704
|
}
|
|
1705
|
+
case 'kg': {
|
|
1706
|
+
// Knowledge graph CRUD — port từ mcp-knowledge-graph, lưu .noob/kg.jsonl.
|
|
1707
|
+
// Sub-cmd: list, path, add, obs, link, unlink, unobs, get, search, rm.
|
|
1708
|
+
const argText = String(arg || '').trim();
|
|
1709
|
+
const m = argText.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
1710
|
+
const sub = m ? m[1].toLowerCase() : '';
|
|
1711
|
+
const rest = m && m[2] ? m[2].trim() : '';
|
|
1712
|
+
try {
|
|
1713
|
+
if (!sub || sub === 'list' || sub === 'ls') {
|
|
1714
|
+
const g = await kgLoad();
|
|
1715
|
+
if (!g.entities.length && !g.relations.length) {
|
|
1716
|
+
console.log(c.dim(` KG rỗng (${kgFilePath()})`));
|
|
1717
|
+
} else {
|
|
1718
|
+
console.log(kgFormat(g));
|
|
1719
|
+
}
|
|
1720
|
+
} else if (sub === 'path') {
|
|
1721
|
+
console.log(c.dim(' ') + kgFilePath());
|
|
1722
|
+
} else if (sub === 'add') {
|
|
1723
|
+
const m2 = rest.match(/^(\S+)\s+(\S+)(?:\s+([\s\S]+))?$/);
|
|
1724
|
+
if (!m2) {
|
|
1725
|
+
console.log(c.err(' Cú pháp: /kg add <Name> <type> [<obs1>; <obs2>; ...]'));
|
|
1726
|
+
} else {
|
|
1727
|
+
const [, name, etype, obsRaw] = m2;
|
|
1728
|
+
const observations = obsRaw
|
|
1729
|
+
? obsRaw.split(';').map((s) => s.trim()).filter(Boolean)
|
|
1730
|
+
: [];
|
|
1731
|
+
const created = await kgCreateEntities([{ name, entityType: etype, observations }]);
|
|
1732
|
+
if (!created.length) console.log(c.dim(` Entity '${name}' đã tồn tại — không tạo lại`));
|
|
1733
|
+
else console.log(c.ok(' ✓ ') + `đã thêm ${name} [${etype}] (${observations.length} obs)`);
|
|
1734
|
+
}
|
|
1735
|
+
} else if (sub === 'obs') {
|
|
1736
|
+
const m2 = rest.match(/^(\S+)\s+([\s\S]+)$/);
|
|
1737
|
+
if (!m2) {
|
|
1738
|
+
console.log(c.err(' Cú pháp: /kg obs <Name> <observation text>'));
|
|
1739
|
+
} else {
|
|
1740
|
+
const [, name, obs] = m2;
|
|
1741
|
+
const out = await kgAddObservations([{ entityName: name, contents: [obs] }]);
|
|
1742
|
+
const added = out[0]?.addedObservations || [];
|
|
1743
|
+
if (!added.length) console.log(c.dim(` Observation đã có cho '${name}' — không thêm lại`));
|
|
1744
|
+
else console.log(c.ok(' ✓ ') + `đã thêm obs cho ${name}: ${added[0]}`);
|
|
1745
|
+
}
|
|
1746
|
+
} else if (sub === 'link') {
|
|
1747
|
+
const parts = rest.split(/\s+/).filter(Boolean);
|
|
1748
|
+
if (parts.length < 3) {
|
|
1749
|
+
console.log(c.err(' Cú pháp: /kg link <From> <verb> <To>'));
|
|
1750
|
+
} else {
|
|
1751
|
+
const [from, verb, ...toParts] = parts;
|
|
1752
|
+
const to = toParts.join(' ');
|
|
1753
|
+
const created = await kgCreateRelations([{ from, to, relationType: verb }]);
|
|
1754
|
+
if (!created.length) console.log(c.dim(' Relation đã tồn tại — không tạo lại'));
|
|
1755
|
+
else console.log(c.ok(' ✓ ') + `${from} --${verb}--> ${to}`);
|
|
1756
|
+
}
|
|
1757
|
+
} else if (sub === 'unlink') {
|
|
1758
|
+
const parts = rest.split(/\s+/).filter(Boolean);
|
|
1759
|
+
if (parts.length < 3) {
|
|
1760
|
+
console.log(c.err(' Cú pháp: /kg unlink <From> <verb> <To>'));
|
|
1761
|
+
} else {
|
|
1762
|
+
const [from, verb, ...toParts] = parts;
|
|
1763
|
+
const to = toParts.join(' ');
|
|
1764
|
+
const out = await kgDeleteRelations([{ from, to, relationType: verb }]);
|
|
1765
|
+
if (!out.deleted) console.log(c.dim(' Không có relation nào khớp'));
|
|
1766
|
+
else console.log(c.ok(' ✓ ') + `xóa relation ${from} --${verb}--> ${to}`);
|
|
1767
|
+
}
|
|
1768
|
+
} else if (sub === 'unobs') {
|
|
1769
|
+
const m2 = rest.match(/^(\S+)\s+([\s\S]+)$/);
|
|
1770
|
+
if (!m2) {
|
|
1771
|
+
console.log(c.err(' Cú pháp: /kg unobs <Name> <observation text>'));
|
|
1772
|
+
} else {
|
|
1773
|
+
const [, name, obs] = m2;
|
|
1774
|
+
await kgDeleteObservations([{ entityName: name, observations: [obs] }]);
|
|
1775
|
+
console.log(c.ok(' ✓ ') + `xóa obs '${obs}' khỏi ${name} (silent nếu không có)`);
|
|
1776
|
+
}
|
|
1777
|
+
} else if (sub === 'get') {
|
|
1778
|
+
if (!rest) {
|
|
1779
|
+
console.log(c.err(' Cú pháp: /kg get <Name1> [<Name2> ...]'));
|
|
1780
|
+
} else {
|
|
1781
|
+
const names = rest.split(/\s+/).filter(Boolean);
|
|
1782
|
+
const g = await kgOpenNodes(names);
|
|
1783
|
+
if (!g.entities.length) console.log(c.dim(` Không tìm thấy entity: ${names.join(', ')}`));
|
|
1784
|
+
else console.log(kgFormat(g));
|
|
1785
|
+
}
|
|
1786
|
+
} else if (sub === 'search') {
|
|
1787
|
+
if (!rest) {
|
|
1788
|
+
console.log(c.err(' Cú pháp: /kg search <query>'));
|
|
1789
|
+
} else {
|
|
1790
|
+
const g = await kgSearchNodes(rest);
|
|
1791
|
+
if (!g.entities.length) console.log(c.dim(` Không có kết quả cho "${rest}"`));
|
|
1792
|
+
else console.log(kgFormat(g));
|
|
1793
|
+
}
|
|
1794
|
+
} else if (sub === 'rm' || sub === 'remove' || sub === 'delete') {
|
|
1795
|
+
if (!rest) {
|
|
1796
|
+
console.log(c.err(' Cú pháp: /kg rm <Name1> [<Name2> ...]'));
|
|
1797
|
+
} else {
|
|
1798
|
+
const names = rest.split(/\s+/).filter(Boolean);
|
|
1799
|
+
const out = await kgDeleteEntities(names);
|
|
1800
|
+
console.log(c.ok(' ✓ ') + `đã xóa ${out.deleted} entity (cascade relation)`);
|
|
1801
|
+
}
|
|
1802
|
+
} else {
|
|
1803
|
+
console.log(
|
|
1804
|
+
c.err(' Sub-command không nhận diện. ') +
|
|
1805
|
+
c.dim('Dùng: list | path | add | obs | link | unlink | unobs | get | search | rm')
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
} catch (e) {
|
|
1809
|
+
if (e instanceof KGEntityNotFound) {
|
|
1810
|
+
console.log(c.err(' ✗ ') + e.message + c.dim(' (gõ /kg add trước khi /kg obs)'));
|
|
1811
|
+
} else if (e instanceof KGMarkerError) {
|
|
1812
|
+
console.log(c.err(' ✗ ') + e.message);
|
|
1813
|
+
} else {
|
|
1814
|
+
console.log(c.err(' ✗ ') + (e?.message || String(e)));
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
break;
|
|
1818
|
+
}
|
|
1704
1819
|
case 'goal': {
|
|
1705
1820
|
// HARD GOAL = completion requirement (xem tweet_dump.txt mục "Combine
|
|
1706
1821
|
// with /goal and /loop"). Set xong sẽ inject vào MỌI prompt tới khi clear.
|
|
@@ -1808,6 +1923,10 @@ NGUYÊN TẮC:
|
|
|
1808
1923
|
persist(); // giữ lại phiên cũ trên đĩa
|
|
1809
1924
|
state.history = [];
|
|
1810
1925
|
state._longSessionWarned = false; // reset cờ cảnh báo phiên dài
|
|
1926
|
+
// Reset toàn bộ permission scopes — task mới hoàn toàn, không kế thừa.
|
|
1927
|
+
state.autoApprove.clear();
|
|
1928
|
+
state.autoApproveTurn.clear();
|
|
1929
|
+
state.autoApproveFile.clear();
|
|
1811
1930
|
startFresh(); // phiên mới (phiên cũ vẫn resume được)
|
|
1812
1931
|
if (!tui.tty) console.clear();
|
|
1813
1932
|
banner();
|
package/src/sessions.js
CHANGED
|
@@ -76,6 +76,7 @@ const normDir = (p) => {
|
|
|
76
76
|
*/
|
|
77
77
|
export function list(limit = 30, cwd = null) {
|
|
78
78
|
ensure();
|
|
79
|
+
maybeAutoPrune();
|
|
79
80
|
const want = cwd != null ? normDir(cwd) : null;
|
|
80
81
|
let files;
|
|
81
82
|
try {
|
|
@@ -120,4 +121,92 @@ export function remove(id) {
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
// Cleanup policy: giữ N phiên gần nhất + xoá phiên già hơn maxAgeDays. Mặc định
|
|
125
|
+
// giữ 50, xoá > 30 ngày — đủ "--continue" + "/sessions" hoạt động trơn cho
|
|
126
|
+
// workflow vài tuần, không phình ổ cứng.
|
|
127
|
+
//
|
|
128
|
+
// opts:
|
|
129
|
+
// keep : số phiên gần nhất tối thiểu phải giữ (default 50)
|
|
130
|
+
// maxAgeDays : phiên cũ hơn ngần này thì xoá NGAY CẢ trong top N (default 30)
|
|
131
|
+
// dryRun : true → không xoá, chỉ trả danh sách dự kiến
|
|
132
|
+
//
|
|
133
|
+
// Trả về {scanned, kept, deleted, errors, deletedIds}.
|
|
134
|
+
export function prune(opts = {}) {
|
|
135
|
+
const keep = Math.max(1, Number(opts.keep) || 50);
|
|
136
|
+
const maxAgeDays = Math.max(1, Number(opts.maxAgeDays) || 30);
|
|
137
|
+
const dryRun = !!opts.dryRun;
|
|
138
|
+
ensure();
|
|
139
|
+
let files = [];
|
|
140
|
+
try {
|
|
141
|
+
files = fs.readdirSync(DIR).filter((f) => f.endsWith('.json'));
|
|
142
|
+
} catch {
|
|
143
|
+
return { scanned: 0, kept: 0, deleted: 0, errors: 0, deletedIds: [] };
|
|
144
|
+
}
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const ageCutoff = now - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
147
|
+
const items = [];
|
|
148
|
+
let parseErrors = 0;
|
|
149
|
+
for (const f of files) {
|
|
150
|
+
const full = path.join(DIR, f);
|
|
151
|
+
try {
|
|
152
|
+
const txt = fs.readFileSync(full, 'utf8');
|
|
153
|
+
const s = JSON.parse(txt);
|
|
154
|
+
const updatedAt = s.updatedAt || s.createdAt || 0;
|
|
155
|
+
items.push({ id: s.id || f.replace(/\.json$/, ''), file: full, updatedAt, broken: false });
|
|
156
|
+
} catch {
|
|
157
|
+
let mtime = 0;
|
|
158
|
+
try {
|
|
159
|
+
mtime = fs.statSync(full).mtimeMs;
|
|
160
|
+
} catch {}
|
|
161
|
+
items.push({ id: f.replace(/\.json$/, ''), file: full, updatedAt: mtime, broken: true });
|
|
162
|
+
parseErrors++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
items.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
166
|
+
const toDelete = [];
|
|
167
|
+
for (let i = 0; i < items.length; i++) {
|
|
168
|
+
const it = items[i];
|
|
169
|
+
const tooOld = it.updatedAt > 0 && it.updatedAt < ageCutoff;
|
|
170
|
+
const overCap = i >= keep;
|
|
171
|
+
if (it.broken || tooOld || overCap) toDelete.push(it);
|
|
172
|
+
}
|
|
173
|
+
let unlinkErrors = 0;
|
|
174
|
+
let deleted = 0;
|
|
175
|
+
const deletedIds = [];
|
|
176
|
+
if (!dryRun) {
|
|
177
|
+
for (const it of toDelete) {
|
|
178
|
+
try {
|
|
179
|
+
fs.unlinkSync(it.file);
|
|
180
|
+
deleted++;
|
|
181
|
+
deletedIds.push(it.id);
|
|
182
|
+
} catch {
|
|
183
|
+
unlinkErrors++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const reportedDeleted = dryRun ? toDelete.length : deleted;
|
|
188
|
+
return {
|
|
189
|
+
scanned: items.length,
|
|
190
|
+
kept: items.length - reportedDeleted,
|
|
191
|
+
deleted: reportedDeleted,
|
|
192
|
+
errors: parseErrors + unlinkErrors,
|
|
193
|
+
deletedIds,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Auto-prune im lặng khi thư mục phình to (>AUTO_PRUNE_AT file). Gọi từ list()
|
|
198
|
+
// mỗi lần liệt kê — chi phí 1 lần readdir, không đáng kể.
|
|
199
|
+
const AUTO_PRUNE_AT = 100;
|
|
200
|
+
let _autoPruneRan = false;
|
|
201
|
+
function maybeAutoPrune() {
|
|
202
|
+
if (_autoPruneRan) return; // chỉ thử 1 lần / phiên CLI để tránh I/O lặp
|
|
203
|
+
_autoPruneRan = true;
|
|
204
|
+
try {
|
|
205
|
+
const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.json'));
|
|
206
|
+
if (files.length > AUTO_PRUNE_AT) prune({ keep: 50, maxAgeDays: 30 });
|
|
207
|
+
} catch {
|
|
208
|
+
/* best-effort */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
123
212
|
export const sessionsDir = DIR;
|
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
|
+
}
|