@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/CHANGELOG.md +18 -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/todos.js +65 -24
- package/src/repl.js +271 -152
- 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();
|
|
@@ -138,6 +149,53 @@ export async function startRepl(opts = {}) {
|
|
|
138
149
|
return tui.read(prompt);
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
// ── permission prompts (y/n/a) ────────────────────────────────────────
|
|
153
|
+
// PHẢI khai báo Ở ĐÂY (ngay sau `ask`), TRƯỚC bất kỳ function nào có thể
|
|
154
|
+
// gọi tới — trước đây 3 const này nằm rải rác (line ~405, ~1648, ~1655)
|
|
155
|
+
// gây TDZ ReferenceError: "Cannot access 'askAddRoot' before initialization"
|
|
156
|
+
// khi execToolCore / workflowExecute chạy lần đầu. Logic vẫn ở permission.js,
|
|
157
|
+
// chỉ wire prompt UI (tui/ask/pending/...) từ scope startRepl.
|
|
158
|
+
//
|
|
159
|
+
// ĐỪNG DI CHUYỂN khối này lên trên: dependencies cần sẵn — `tui` (~line 112),
|
|
160
|
+
// `pending` (~line 107), `ask` (~line 136), `c`/`t`/`truncate` (import top).
|
|
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ệ.
|
|
162
|
+
const askPermission = (name, targetPath) =>
|
|
163
|
+
_askPermission(name, { tui, ask, pending, c, t, truncate, targetPath });
|
|
164
|
+
const askAddRoot = (root, targetPath) =>
|
|
165
|
+
_askAddRoot(root, targetPath, { tui, ask, pending, c, t, truncate });
|
|
166
|
+
const askWorkflowAgentMode = () =>
|
|
167
|
+
_askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
|
|
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
|
+
|
|
141
199
|
// NOOB_DEBUG=1: vạch trần đường thoát thật sự (xác nhận/loại bỏ giả thuyết
|
|
142
200
|
// "event loop cạn"). 'beforeExit' nổ = loop cạn (stdin chết). 'exit' = thoát.
|
|
143
201
|
if (process.env.NOOB_DEBUG === '1') {
|
|
@@ -399,15 +457,12 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
|
|
|
399
457
|
await workflowExecute(trimmed);
|
|
400
458
|
}
|
|
401
459
|
|
|
402
|
-
// Hỏi quyền bật agent mode để chạy workflow. CHỈ chấp nhận y/n (Enter = yes).
|
|
403
|
-
// Nếu nhận dòng lạ & dài (paste nhầm tin nhắn) → xếp hàng + hỏi lại (y hệt
|
|
404
|
-
// askPermission / askAddRoot) để user khỏi phải gõ lại /workflow.
|
|
405
|
-
const askWorkflowAgentMode = () => _askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
|
|
406
|
-
|
|
407
460
|
// Chạy thật workflow prompt — chia sẻ giữa ad-hoc và `run <name>`.
|
|
408
461
|
// `builtInName` (optional): nếu có thì SKIP loadSkill dynamic-workflows (prompt
|
|
409
462
|
// built-in đã hardcode pattern + step cụ thể rồi, không cần model design lại).
|
|
410
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;
|
|
411
466
|
let prompt;
|
|
412
467
|
if (builtInName) {
|
|
413
468
|
// Built-in prompt đã đầy đủ PLAN + 4 bước thực thi — KHÔNG wrap thêm skill.
|
|
@@ -440,7 +495,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
440
495
|
2. Spawn sub-agent theo plan — mỗi prompt sub-agent có 5 mục GOAL/INPUTS/METHOD/OUTPUT SHAPE/STOP CONDITION.
|
|
441
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.`;
|
|
442
497
|
}
|
|
443
|
-
if (!state.
|
|
498
|
+
if (!state.agentMode) {
|
|
444
499
|
// Đừng tự bật — workflow cần spawn_agent, đây là quyền nặng (sub-agent chạy
|
|
445
500
|
// tool độc lập). Hỏi 1 lần, user chọn y thì bật & chạy, n thì huỷ sạch +
|
|
446
501
|
// gợi ý /agent. Tránh buộc user gõ lại /workflow sau khi /agent.
|
|
@@ -454,7 +509,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
454
509
|
)
|
|
455
510
|
);
|
|
456
511
|
}
|
|
457
|
-
state.
|
|
512
|
+
state.agentMode = true;
|
|
458
513
|
console.log(
|
|
459
514
|
c.tool(' ✓ ' + (t.workflowAgentEnabled || 'đã bật agent mode cho workflow này.'))
|
|
460
515
|
);
|
|
@@ -782,6 +837,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
782
837
|
async function runUltra(goal) {
|
|
783
838
|
if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
|
|
784
839
|
if (!goal) return console.log(c.err(' ' + t.ultraNeedGoal));
|
|
840
|
+
if (!(await checkQuotaBeforeHeavy('/ultra'))) return;
|
|
785
841
|
state.mode = 'chat'; // tự hành chỉ chạy ở chế độ agent
|
|
786
842
|
state.ultra = true;
|
|
787
843
|
console.log(c.accent(' 🚀 ' + t.ultraOn));
|
|
@@ -954,6 +1010,7 @@ Sau khi xong tick này, TỰ ĐÁNH GIÁ:
|
|
|
954
1010
|
if (!task) return console.log(c.err(' ' + t.loopNeedArgs));
|
|
955
1011
|
const intervalMs = parseInterval(intervalStr);
|
|
956
1012
|
if (!intervalMs) return console.log(c.err(' ' + t.loopBadInterval(intervalStr)));
|
|
1013
|
+
if (!(await checkQuotaBeforeHeavy('/loop'))) return;
|
|
957
1014
|
const normInterval = fmtMs(intervalMs);
|
|
958
1015
|
state.loop = {
|
|
959
1016
|
intervalMs,
|
|
@@ -1090,6 +1147,50 @@ NGUYÊN TẮC:
|
|
|
1090
1147
|
console.log(c.dim(' ' + memoryPath() + t.memoryStat(mem.split('\n').length)));
|
|
1091
1148
|
}
|
|
1092
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
|
+
|
|
1093
1194
|
// /auto-yolo — lưu/bỏ yolo làm MẶC ĐỊNH (mỗi lần mở noob tự bật). Vì yolo tự
|
|
1094
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.
|
|
1095
1196
|
async function toggleAutoYolo() {
|
|
@@ -1312,126 +1413,13 @@ NGUYÊN TẮC:
|
|
|
1312
1413
|
startSpin(t.thinking);
|
|
1313
1414
|
let printer = null;
|
|
1314
1415
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
result: 'ERROR: agent mode đang TẮT — gõ /agent on để bật trước khi spawn.',
|
|
1323
|
-
};
|
|
1324
|
-
if (depth >= MAX_SUBAGENT_DEPTH)
|
|
1325
|
-
return {
|
|
1326
|
-
allow: true,
|
|
1327
|
-
result: `ERROR: đã đạt depth tối đa (${MAX_SUBAGENT_DEPTH}) — không spawn thêm.`,
|
|
1328
|
-
};
|
|
1329
|
-
const tasks =
|
|
1330
|
-
name === 'spawn_agent' ? [input] : Array.isArray(input?.agents) ? input.agents : [];
|
|
1331
|
-
if (!tasks.length) return { allow: true, result: 'ERROR: thiếu task cho sub-agent.' };
|
|
1332
|
-
stopSpin();
|
|
1333
|
-
console.log(
|
|
1334
|
-
chalk.hex('#8b5cf6')(
|
|
1335
|
-
` ⊕ spawn ${tasks.length} sub-agent (depth ${depth + 1}/${MAX_SUBAGENT_DEPTH})`
|
|
1336
|
-
)
|
|
1337
|
-
);
|
|
1338
|
-
startSpin(t.thinking);
|
|
1339
|
-
try {
|
|
1340
|
-
const runData = state.workflowRun?.data || null;
|
|
1341
|
-
const results = await Promise.all(
|
|
1342
|
-
tasks.map((task, i) => {
|
|
1343
|
-
// Per-sub-agent model routing: task.model có thể là id model hoặc tên thân thiện.
|
|
1344
|
-
// findModel() resolve cả hai; nếu không match thì fallback model của cha.
|
|
1345
|
-
let subModel = state.model.id;
|
|
1346
|
-
let modelTag = '';
|
|
1347
|
-
if (task?.model) {
|
|
1348
|
-
const m = findModel(task.model);
|
|
1349
|
-
if (m) {
|
|
1350
|
-
subModel = m.id;
|
|
1351
|
-
modelTag = ` [${m.name}]`;
|
|
1352
|
-
} else
|
|
1353
|
-
modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
|
|
1354
|
-
}
|
|
1355
|
-
const taskBody = task?.task || task?.prompt || '';
|
|
1356
|
-
const taskCtx = task?.context || '';
|
|
1357
|
-
// Workflow journal: nếu đang trong run + task đã done lần trước → return
|
|
1358
|
-
// cached result, tiết kiệm token. Hash = crc32(task+ctx+model).
|
|
1359
|
-
if (runData) {
|
|
1360
|
-
const hash = hashWorkflowTask({ task: taskBody, context: taskCtx, model: subModel });
|
|
1361
|
-
const cached = lookupWorkflowTaskResult(runData, hash);
|
|
1362
|
-
if (cached !== null) {
|
|
1363
|
-
stopSpin();
|
|
1364
|
-
console.log(
|
|
1365
|
-
chalk.hex('#8b5cf6')(
|
|
1366
|
-
` ⊘ sub-agent #${i + 1}${modelTag} skip — đã done trong run trước (cached)`
|
|
1367
|
-
)
|
|
1368
|
-
);
|
|
1369
|
-
startSpin(t.thinking);
|
|
1370
|
-
return Promise.resolve(
|
|
1371
|
-
`── sub-agent #${i + 1}${modelTag} (cached) ──\n${cached}`
|
|
1372
|
-
);
|
|
1373
|
-
}
|
|
1374
|
-
recordWorkflowTaskStart(runData, {
|
|
1375
|
-
hash,
|
|
1376
|
-
task: taskBody,
|
|
1377
|
-
context: taskCtx,
|
|
1378
|
-
model: subModel,
|
|
1379
|
-
});
|
|
1380
|
-
// [GỠ BUDGET 2026-06-06] Sub-agent chạy không giới hạn token.
|
|
1381
|
-
return runSubAgent({
|
|
1382
|
-
task: taskBody,
|
|
1383
|
-
context: taskCtx,
|
|
1384
|
-
depth: depth + 1,
|
|
1385
|
-
model: subModel,
|
|
1386
|
-
signal: abort.signal,
|
|
1387
|
-
tokenMeter,
|
|
1388
|
-
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
1389
|
-
onLog: (msg) => {
|
|
1390
|
-
stopSpin();
|
|
1391
|
-
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
1392
|
-
startSpin(t.thinking);
|
|
1393
|
-
},
|
|
1394
|
-
})
|
|
1395
|
-
.then((r) => {
|
|
1396
|
-
recordWorkflowTaskDone(runData, hash, r);
|
|
1397
|
-
return `── sub-agent #${i + 1}${modelTag} ──\n${r}`;
|
|
1398
|
-
})
|
|
1399
|
-
.catch((e) => {
|
|
1400
|
-
recordWorkflowTaskFailed(runData, hash, e);
|
|
1401
|
-
return `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`;
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
// Không có active workflow run → behavior cũ.
|
|
1405
|
-
return runSubAgent({
|
|
1406
|
-
task: taskBody,
|
|
1407
|
-
context: taskCtx,
|
|
1408
|
-
depth: depth + 1,
|
|
1409
|
-
model: subModel,
|
|
1410
|
-
signal: abort.signal,
|
|
1411
|
-
tokenMeter,
|
|
1412
|
-
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
1413
|
-
onLog: (msg) => {
|
|
1414
|
-
stopSpin();
|
|
1415
|
-
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
1416
|
-
startSpin(t.thinking);
|
|
1417
|
-
},
|
|
1418
|
-
})
|
|
1419
|
-
.then((r) => `── sub-agent #${i + 1}${modelTag} ──\n${r}`)
|
|
1420
|
-
.catch(
|
|
1421
|
-
(e) => `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`
|
|
1422
|
-
);
|
|
1423
|
-
})
|
|
1424
|
-
);
|
|
1425
|
-
return { allow: true, result: results.join('\n\n') };
|
|
1426
|
-
} catch (err) {
|
|
1427
|
-
return { allow: true, result: 'ERROR sub-agent: ' + (err?.message || String(err)) };
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
stopSpin();
|
|
1431
|
-
const res = await execTool(name, input);
|
|
1432
|
-
startSpin(t.thinking);
|
|
1433
|
-
return res;
|
|
1434
|
-
};
|
|
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
|
+
});
|
|
1435
1423
|
|
|
1436
1424
|
const answer = await runAgent({
|
|
1437
1425
|
history: state.history,
|
|
@@ -1483,6 +1471,9 @@ NGUYÊN TẮC:
|
|
|
1483
1471
|
} finally {
|
|
1484
1472
|
abort = null;
|
|
1485
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();
|
|
1486
1477
|
// Auto-compact dựa trên context tokens thay vì chars.
|
|
1487
1478
|
// Với CONTEXT_WINDOW = 2M tokens (xem src/tokens.js):
|
|
1488
1479
|
// 75% (1.5M tokens) → auto compact
|
|
@@ -1592,12 +1583,34 @@ NGUYÊN TẮC:
|
|
|
1592
1583
|
}
|
|
1593
1584
|
}
|
|
1594
1585
|
|
|
1595
|
-
if (DESTRUCTIVE.has(name) && !state.yolo
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
+
}
|
|
1601
1614
|
}
|
|
1602
1615
|
}
|
|
1603
1616
|
|
|
@@ -1642,18 +1655,6 @@ NGUYÊN TẮC:
|
|
|
1642
1655
|
}
|
|
1643
1656
|
}
|
|
1644
1657
|
|
|
1645
|
-
// Hỏi user có muốn cấp quyền folder ngoài workspace cho tool call hiện tại
|
|
1646
|
-
// hay không. Trả về "y" | "n" | "a" (luôn). y = chỉ lần này; a = auto-approve
|
|
1647
|
-
// mọi add-root trong phiên. Cùng style retry với askPermission (lạc → queue).
|
|
1648
|
-
const askAddRoot = (root, targetPath) => _askAddRoot(root, targetPath, { tui, ask, pending, c, t, truncate });
|
|
1649
|
-
|
|
1650
|
-
// Đọc câu trả lời cho phép một cách CHẮC CHẮN. Chỉ chấp nhận y/n/a (hoặc
|
|
1651
|
-
// Enter = đồng ý). Nếu nhận một dòng lạ & dài — gần như chắc là một tin nhắn
|
|
1652
|
-
// bị paste/gõ nhầm vào đúng lúc prompt hiện ra (đây là thủ phạm làm hỏng &
|
|
1653
|
-
// "tự tắt" trước đây) — thì KHÔNG coi là từ chối: xếp nó vào hàng đợi tin
|
|
1654
|
-
// nhắn rồi HỎI LẠI. Nhờ vậy không thao tác nào bị quyết định bởi rác.
|
|
1655
|
-
const askPermission = (name) => _askPermission(name, { tui, ask, pending, c, t, truncate });
|
|
1656
|
-
|
|
1657
1658
|
// ── slash commands ─────────────────────────────────────────────────────
|
|
1658
1659
|
async function command(input) {
|
|
1659
1660
|
const [cmd, ...rest] = input.slice(1).split(/\s+/);
|
|
@@ -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;
|