@noobdemon/noob-cli 1.13.0 → 1.13.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 +26 -0
- package/README.md +139 -106
- package/package.json +3 -2
- package/src/agent.js +66 -9
- package/src/api.js +11 -1
- package/src/diff.js +40 -6
- package/src/i18n.js +0 -14
- package/src/repl/commands/goal.js +37 -0
- package/src/repl/commands/kg.js +147 -0
- package/src/repl.js +36 -147
- package/src/tools.js +22 -6
- package/src/tui.js +128 -29
package/src/diff.js
CHANGED
|
@@ -8,6 +8,42 @@
|
|
|
8
8
|
// File lớn hơn (write_file overwrite 1k+ dòng) → caller truncate trước khi gọi.
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
|
|
11
|
+
// Bề rộng khả dụng cho NỘI DUNG diff (trừ tiền tố ' │ ' = 4 cột + '+ '/'- ' = 2).
|
|
12
|
+
// Cap tối thiểu 20 để không wrap loạn ở terminal hẹp; mặc định 80 khi không phải TTY.
|
|
13
|
+
function contentWidth() {
|
|
14
|
+
const cols = process.stdout.columns || 80;
|
|
15
|
+
return Math.max(20, cols - 6);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Soft-wrap 1 đoạn text THUẦN (không màu) thành mảng dòng có vis-width ≤ width.
|
|
19
|
+
// Ưu tiên word boundary; không có space hợp lý → hard-slice. Diff text đã bị
|
|
20
|
+
// slice(0,200) ở caller nên không cần ANSI-aware ở đây.
|
|
21
|
+
function wrapPlain(text, width) {
|
|
22
|
+
if (text.length <= width) return [text];
|
|
23
|
+
const lines = [];
|
|
24
|
+
let remaining = text;
|
|
25
|
+
while (remaining.length > width) {
|
|
26
|
+
let cut = width;
|
|
27
|
+
const lastSpace = remaining.slice(0, width).lastIndexOf(' ');
|
|
28
|
+
if (lastSpace > width * 0.3) cut = lastSpace;
|
|
29
|
+
lines.push(remaining.slice(0, cut).trimEnd());
|
|
30
|
+
remaining = remaining.slice(cut).trimStart();
|
|
31
|
+
}
|
|
32
|
+
if (remaining) lines.push(remaining);
|
|
33
|
+
return lines.length ? lines : [''];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Emit 1 dòng diff đã wrap: dòng đầu có dấu (+/-/space), dòng wrap kế thụt 2 cột
|
|
37
|
+
// cho thẳng cột nội dung. Mỗi dòng con tự bọc tiền tố '│' + tô màu riêng (chalk
|
|
38
|
+
// reset cuối từng đoạn) → wrap KHÔNG mất màu / mất khung.
|
|
39
|
+
function emitDiffLine(out, text, color, sign) {
|
|
40
|
+
const segs = wrapPlain(text, contentWidth());
|
|
41
|
+
for (let i = 0; i < segs.length; i++) {
|
|
42
|
+
const prefix = i === 0 ? sign : ' ';
|
|
43
|
+
out.push(chalk.dim(' │ ') + color(prefix + segs[i]));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
11
47
|
// LCS table giữa 2 mảng lines. Trả về matrix (M+1) x (N+1).
|
|
12
48
|
function lcsTable(a, b) {
|
|
13
49
|
const m = a.length;
|
|
@@ -129,11 +165,9 @@ export function renderUnifiedDiff(
|
|
|
129
165
|
continue;
|
|
130
166
|
}
|
|
131
167
|
const text = String(op.text).slice(0, 200);
|
|
132
|
-
|
|
133
|
-
if (op.type === '
|
|
134
|
-
else
|
|
135
|
-
else line = chalk.dim(' ') + chalk.dim(text);
|
|
136
|
-
out.push(chalk.dim(' │ ') + line);
|
|
168
|
+
if (op.type === 'add') emitDiffLine(out, text, chalk.green, '+ ');
|
|
169
|
+
else if (op.type === 'del') emitDiffLine(out, text, chalk.red, '- ');
|
|
170
|
+
else emitDiffLine(out, text, chalk.dim, ' ');
|
|
137
171
|
printed++;
|
|
138
172
|
}
|
|
139
173
|
}
|
|
@@ -153,7 +187,7 @@ export function renderNewFilePreview(content, { label = '', maxLines = 20 } = {}
|
|
|
153
187
|
const out = [];
|
|
154
188
|
out.push(chalk.dim(' ┌─ ' + label + ' ') + chalk.green(`(file MỚI · ${lines.length} dòng)`));
|
|
155
189
|
for (const l of head) {
|
|
156
|
-
out.
|
|
190
|
+
emitDiffLine(out, l.slice(0, 200), chalk.green, '+ ');
|
|
157
191
|
}
|
|
158
192
|
if (more > 0) out.push(chalk.dim(' │ ') + chalk.dim(`… +${more} dòng nữa`));
|
|
159
193
|
out.push(chalk.dim(' └─'));
|
package/src/i18n.js
CHANGED
|
@@ -26,7 +26,6 @@ export const t = {
|
|
|
26
26
|
// auth
|
|
27
27
|
notLoggedIn:
|
|
28
28
|
'Bạn chưa đăng nhập. Chạy: noob login <api-key>\nChưa có key? Liên hệ admin để lấy key (Pro / Pro+ / Trial).',
|
|
29
|
-
loginOk: (plan) => `Đăng nhập thành công. Gói: ${plan}.`,
|
|
30
29
|
loginSaved: (p) => `Đã lưu API key vào ${p}`,
|
|
31
30
|
loggedOut: 'Đã đăng xuất, xoá API key khỏi máy.',
|
|
32
31
|
needKeyArg: 'Thiếu key. Dùng: noob login <api-key>',
|
|
@@ -173,11 +172,6 @@ export const t = {
|
|
|
173
172
|
loopAutoStop: (n) => `Loop tự dừng sau tick #${n} — model phát <<LOOP_DONE>> (task hoàn tất).`,
|
|
174
173
|
loopAlreadyRunning: 'Đã có loop đang chạy. /loop stop trước khi đặt loop mới.',
|
|
175
174
|
learning: 'đang chưng cất bài học vào noob.md…',
|
|
176
|
-
learnSuggest: (n) =>
|
|
177
|
-
`💡 Phiên này có ${n} lượt. Gõ /learn trước để chưng cất bài học vào noob.md (sau khi /new thì history sẽ mất).`,
|
|
178
|
-
memoryStatus: (lines, rules, notes, ago) =>
|
|
179
|
-
`📝 noob.md: ${lines} dòng (${rules} rules, ${notes} notes) · cập nhật ${ago}`,
|
|
180
|
-
memoryMissing: '📝 noob.md: chưa có — gõ /init để tạo từ dự án.',
|
|
181
175
|
compactRunning: 'đang tóm tắt phiên để gọn ngữ cảnh…',
|
|
182
176
|
compactEmpty: 'Phiên còn trống — không có gì để tóm tắt.',
|
|
183
177
|
compactSkipped: 'Phiên còn ngắn hoặc tóm tắt thất bại — bỏ qua.',
|
|
@@ -199,12 +193,8 @@ export const t = {
|
|
|
199
193
|
'cần mô tả yêu cầu. Ví dụ: /frontend-design landing page cho app nghe nhạc lo-fi',
|
|
200
194
|
workflowRunning: 'đang chạy dynamic workflow đa sub-agent…',
|
|
201
195
|
workflowNoSkill: 'không tìm thấy skills/dynamic-workflows/SKILL.md — skill chưa được cài.',
|
|
202
|
-
workflowNeedArg: 'cần mô tả task. Ví dụ: /workflow audit toàn bộ src/ tìm lỗ hổng SQL injection',
|
|
203
|
-
workflowAgentAutoOn: 'agent mode tự bật cho /workflow (cần spawn_agent)',
|
|
204
196
|
workflowAgentAskHint:
|
|
205
197
|
'🎼 /workflow cần spawn sub-agent (spawn_agent) — agent mode hiện đang TẮT.',
|
|
206
|
-
workflowAgentAskPrompt:
|
|
207
|
-
' bật agent mode và chạy workflow? [y] có, bật & chạy / [n] huỷ (gõ /agent rồi chạy lại nếu muốn) › ',
|
|
208
198
|
workflowAgentEnabled: 'đã bật agent mode cho workflow này.',
|
|
209
199
|
bgWorkflowDone: (label) => `workflow nền ${label} đã xong`,
|
|
210
200
|
bgWorkflowFailed: (label, err) => `workflow nền ${label} lỗi: ${err}`,
|
|
@@ -212,8 +202,6 @@ export const t = {
|
|
|
212
202
|
workflowAgentDenied:
|
|
213
203
|
'đã huỷ /workflow — agent mode vẫn TẮT. Gõ /agent rồi chạy lại lệnh nếu muốn.',
|
|
214
204
|
// saved workflows (CRUD)
|
|
215
|
-
workflowListEmpty: (dir) =>
|
|
216
|
-
`Chưa có workflow đã lưu. Tạo bằng /workflow save <name> <yêu cầu>. Thư mục: ${dir}`,
|
|
217
205
|
workflowListHeader: (dir) => `Workflow đã lưu (${dir}):`,
|
|
218
206
|
workflowSaveNeedArgs: 'Cách dùng: /workflow save <name> <yêu cầu workflow>',
|
|
219
207
|
workflowSaveEmptyPrompt:
|
|
@@ -228,7 +216,6 @@ export const t = {
|
|
|
228
216
|
workflowSaveDescOk: (n, d) => `Đã thêm mô tả cho '${n}': ${d}`,
|
|
229
217
|
workflowRunNeedName: 'Cách dùng: /workflow run <name> [thêm ngữ cảnh]',
|
|
230
218
|
workflowRunError: (n, e) => `Không nạp được workflow '${n}': ${e}`,
|
|
231
|
-
workflowRunOk: (n) => `Chạy workflow đã lưu '${n}'…`,
|
|
232
219
|
workflowRunPreviewBuiltin: (n, title) => `Built-in workflow '${n}' (${title})`,
|
|
233
220
|
workflowRunPreviewSaved: (n) => `Workflow đã lưu '${n}'`,
|
|
234
221
|
workflowLoadNeedName: 'Cách dùng: /workflow load <name>',
|
|
@@ -267,7 +254,6 @@ export const t = {
|
|
|
267
254
|
updateBgDone: 'Đang cập nhật nền. Mở lại noob để dùng bản mới.',
|
|
268
255
|
updateChecking: 'Đang kiểm tra cập nhật…',
|
|
269
256
|
updateLatest: (cur) => `Đã ở bản mới nhất (${cur}).`,
|
|
270
|
-
updating: 'Đang cập nhật…',
|
|
271
257
|
updateOk: '✓ Cập nhật xong. Mở lại noob để dùng bản mới.',
|
|
272
258
|
updateFail: '✗ Cập nhật thất bại. Thử thủ công: npm i -g @noobdemon/noob-cli@latest',
|
|
273
259
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Slash command /goal — HARD GOAL của phiên (completion requirement).
|
|
2
|
+
// Tách khỏi repl.js để giảm phình god module. Dependency (state, tui, persist,
|
|
3
|
+
// helper màu c) inject qua tham số — không truy cập closure startRepl.
|
|
4
|
+
//
|
|
5
|
+
// Set xong sẽ inject vào MỌI prompt tới khi /goal clear. Cú pháp:
|
|
6
|
+
// /goal → xem goal hiện tại
|
|
7
|
+
// /goal <mục tiêu> → đặt goal
|
|
8
|
+
// /goal clear|off|xoá|xoa → xoá goal
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} args
|
|
12
|
+
* @param {string} args.arg - text sau "/goal "
|
|
13
|
+
* @param {object} args.state - session state (đọc/ghi state.goal)
|
|
14
|
+
* @param {object} args.tui - TUI (tui.setGoal)
|
|
15
|
+
* @param {Function} args.persist - lưu phiên xuống đĩa
|
|
16
|
+
* @param {object} args.c - helper màu (c.accent/c.dim)
|
|
17
|
+
*/
|
|
18
|
+
export function runGoalCommand({ arg, state, tui, persist, c }) {
|
|
19
|
+
const v = String(arg || '').trim();
|
|
20
|
+
if (!v) {
|
|
21
|
+
if (state.goal) console.log(c.accent(' 🎯 goal: ') + state.goal);
|
|
22
|
+
else console.log(c.dim(' chưa đặt goal. Cú pháp: /goal <mục tiêu> · /goal clear để xoá'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const lower = v.toLowerCase();
|
|
26
|
+
if (lower === 'clear' || lower === 'off' || lower === 'xoá' || lower === 'xoa') {
|
|
27
|
+
state.goal = null;
|
|
28
|
+
tui.setGoal('');
|
|
29
|
+
console.log(c.dim(' đã xoá goal'));
|
|
30
|
+
persist();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
state.goal = v;
|
|
34
|
+
tui.setGoal(v);
|
|
35
|
+
console.log(c.accent(' 🎯 đã đặt goal: ') + v);
|
|
36
|
+
persist();
|
|
37
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Slash command /kg — knowledge graph CRUD (port từ mcp-knowledge-graph).
|
|
2
|
+
// Tách khỏi repl.js để giảm độ phình của god module. Mọi dependency (hàm kg*,
|
|
3
|
+
// helper màu c) được inject qua tham số — không truy cập closure startRepl,
|
|
4
|
+
// nên test được độc lập.
|
|
5
|
+
//
|
|
6
|
+
// Sub-cmd: list/ls, path, add, obs, link, unlink, unobs, get, search, rm.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} args
|
|
10
|
+
* @param {string} args.arg - phần text sau "/kg " (vd "add Foo type obs")
|
|
11
|
+
* @param {object} args.c - bảng helper màu (c.ok/c.err/c.dim)
|
|
12
|
+
* @param {object} args.kg - các hàm knowledge graph + error class
|
|
13
|
+
*/
|
|
14
|
+
export async function runKgCommand({ arg, c, kg }) {
|
|
15
|
+
const {
|
|
16
|
+
kgLoad,
|
|
17
|
+
kgCreateEntities,
|
|
18
|
+
kgCreateRelations,
|
|
19
|
+
kgAddObservations,
|
|
20
|
+
kgDeleteEntities,
|
|
21
|
+
kgDeleteObservations,
|
|
22
|
+
kgDeleteRelations,
|
|
23
|
+
kgSearchNodes,
|
|
24
|
+
kgOpenNodes,
|
|
25
|
+
kgFormat,
|
|
26
|
+
kgFilePath,
|
|
27
|
+
KGEntityNotFound,
|
|
28
|
+
KGMarkerError,
|
|
29
|
+
} = kg;
|
|
30
|
+
|
|
31
|
+
const argText = String(arg || '').trim();
|
|
32
|
+
const m = argText.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
33
|
+
const sub = m ? m[1].toLowerCase() : '';
|
|
34
|
+
const rest = m && m[2] ? m[2].trim() : '';
|
|
35
|
+
try {
|
|
36
|
+
if (!sub || sub === 'list' || sub === 'ls') {
|
|
37
|
+
const g = await kgLoad();
|
|
38
|
+
if (!g.entities.length && !g.relations.length) {
|
|
39
|
+
console.log(c.dim(` KG rỗng (${kgFilePath()})`));
|
|
40
|
+
} else {
|
|
41
|
+
console.log(kgFormat(g));
|
|
42
|
+
}
|
|
43
|
+
} else if (sub === 'path') {
|
|
44
|
+
console.log(c.dim(' ') + kgFilePath());
|
|
45
|
+
} else if (sub === 'add') {
|
|
46
|
+
const m2 = rest.match(/^(\S+)\s+(\S+)(?:\s+([\s\S]+))?$/);
|
|
47
|
+
if (!m2) {
|
|
48
|
+
console.log(c.err(' Cú pháp: /kg add <Name> <type> [<obs1>; <obs2>; ...]'));
|
|
49
|
+
} else {
|
|
50
|
+
const [, name, etype, obsRaw] = m2;
|
|
51
|
+
const observations = obsRaw
|
|
52
|
+
? obsRaw
|
|
53
|
+
.split(';')
|
|
54
|
+
.map((s) => s.trim())
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
: [];
|
|
57
|
+
const created = await kgCreateEntities([{ name, entityType: etype, observations }]);
|
|
58
|
+
if (!created.length)
|
|
59
|
+
console.log(c.dim(` Entity '${name}' đã tồn tại — không tạo lại`));
|
|
60
|
+
else
|
|
61
|
+
console.log(c.ok(' ✓ ') + `đã thêm ${name} [${etype}] (${observations.length} obs)`);
|
|
62
|
+
}
|
|
63
|
+
} else if (sub === 'obs') {
|
|
64
|
+
const m2 = rest.match(/^(\S+)\s+([\s\S]+)$/);
|
|
65
|
+
if (!m2) {
|
|
66
|
+
console.log(c.err(' Cú pháp: /kg obs <Name> <observation text>'));
|
|
67
|
+
} else {
|
|
68
|
+
const [, name, obs] = m2;
|
|
69
|
+
const out = await kgAddObservations([{ entityName: name, contents: [obs] }]);
|
|
70
|
+
const added = out[0]?.addedObservations || [];
|
|
71
|
+
if (!added.length)
|
|
72
|
+
console.log(c.dim(` Observation đã có cho '${name}' — không thêm lại`));
|
|
73
|
+
else console.log(c.ok(' ✓ ') + `đã thêm obs cho ${name}: ${added[0]}`);
|
|
74
|
+
}
|
|
75
|
+
} else if (sub === 'link') {
|
|
76
|
+
const parts = rest.split(/\s+/).filter(Boolean);
|
|
77
|
+
if (parts.length < 3) {
|
|
78
|
+
console.log(c.err(' Cú pháp: /kg link <From> <verb> <To>'));
|
|
79
|
+
} else {
|
|
80
|
+
const [from, verb, ...toParts] = parts;
|
|
81
|
+
const to = toParts.join(' ');
|
|
82
|
+
const created = await kgCreateRelations([{ from, to, relationType: verb }]);
|
|
83
|
+
if (!created.length) console.log(c.dim(' Relation đã tồn tại — không tạo lại'));
|
|
84
|
+
else console.log(c.ok(' ✓ ') + `${from} --${verb}--> ${to}`);
|
|
85
|
+
}
|
|
86
|
+
} else if (sub === 'unlink') {
|
|
87
|
+
const parts = rest.split(/\s+/).filter(Boolean);
|
|
88
|
+
if (parts.length < 3) {
|
|
89
|
+
console.log(c.err(' Cú pháp: /kg unlink <From> <verb> <To>'));
|
|
90
|
+
} else {
|
|
91
|
+
const [from, verb, ...toParts] = parts;
|
|
92
|
+
const to = toParts.join(' ');
|
|
93
|
+
const out = await kgDeleteRelations([{ from, to, relationType: verb }]);
|
|
94
|
+
if (!out.deleted) console.log(c.dim(' Không có relation nào khớp'));
|
|
95
|
+
else console.log(c.ok(' ✓ ') + `xóa relation ${from} --${verb}--> ${to}`);
|
|
96
|
+
}
|
|
97
|
+
} else if (sub === 'unobs') {
|
|
98
|
+
const m2 = rest.match(/^(\S+)\s+([\s\S]+)$/);
|
|
99
|
+
if (!m2) {
|
|
100
|
+
console.log(c.err(' Cú pháp: /kg unobs <Name> <observation text>'));
|
|
101
|
+
} else {
|
|
102
|
+
const [, name, obs] = m2;
|
|
103
|
+
await kgDeleteObservations([{ entityName: name, observations: [obs] }]);
|
|
104
|
+
console.log(c.ok(' ✓ ') + `xóa obs '${obs}' khỏi ${name} (silent nếu không có)`);
|
|
105
|
+
}
|
|
106
|
+
} else if (sub === 'get') {
|
|
107
|
+
if (!rest) {
|
|
108
|
+
console.log(c.err(' Cú pháp: /kg get <Name1> [<Name2> ...]'));
|
|
109
|
+
} else {
|
|
110
|
+
const names = rest.split(/\s+/).filter(Boolean);
|
|
111
|
+
const g = await kgOpenNodes(names);
|
|
112
|
+
if (!g.entities.length)
|
|
113
|
+
console.log(c.dim(` Không tìm thấy entity: ${names.join(', ')}`));
|
|
114
|
+
else console.log(kgFormat(g));
|
|
115
|
+
}
|
|
116
|
+
} else if (sub === 'search') {
|
|
117
|
+
if (!rest) {
|
|
118
|
+
console.log(c.err(' Cú pháp: /kg search <query>'));
|
|
119
|
+
} else {
|
|
120
|
+
const g = await kgSearchNodes(rest);
|
|
121
|
+
if (!g.entities.length) console.log(c.dim(` Không có kết quả cho "${rest}"`));
|
|
122
|
+
else console.log(kgFormat(g));
|
|
123
|
+
}
|
|
124
|
+
} else if (sub === 'rm' || sub === 'remove' || sub === 'delete') {
|
|
125
|
+
if (!rest) {
|
|
126
|
+
console.log(c.err(' Cú pháp: /kg rm <Name1> [<Name2> ...]'));
|
|
127
|
+
} else {
|
|
128
|
+
const names = rest.split(/\s+/).filter(Boolean);
|
|
129
|
+
const out = await kgDeleteEntities(names);
|
|
130
|
+
console.log(c.ok(' ✓ ') + `đã xóa ${out.deleted} entity (cascade relation)`);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
console.log(
|
|
134
|
+
c.err(' Sub-command không nhận diện. ') +
|
|
135
|
+
c.dim('Dùng: list | path | add | obs | link | unlink | unobs | get | search | rm')
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (e instanceof KGEntityNotFound) {
|
|
140
|
+
console.log(c.err(' ✗ ') + e.message + c.dim(' (gõ /kg add trước khi /kg obs)'));
|
|
141
|
+
} else if (e instanceof KGMarkerError) {
|
|
142
|
+
console.log(c.err(' ✗ ') + e.message);
|
|
143
|
+
} else {
|
|
144
|
+
console.log(c.err(' ✗ ') + (e?.message || String(e)));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/repl.js
CHANGED
|
@@ -72,6 +72,8 @@ import {
|
|
|
72
72
|
import { getBuiltinWorkflow, loadBuiltinPrompt } from './workflows-builtin.js';
|
|
73
73
|
import { SLASH, completeInput, mentionedFiles } from './repl/complete.js';
|
|
74
74
|
import { parseTodosFromHistory } from './repl/todos.js';
|
|
75
|
+
import { runKgCommand } from './repl/commands/kg.js';
|
|
76
|
+
import { runGoalCommand } from './repl/commands/goal.js';
|
|
75
77
|
import {
|
|
76
78
|
ULTRA_DONE,
|
|
77
79
|
MAX_QUESTS,
|
|
@@ -1566,12 +1568,18 @@ NGUYÊN TẮC:
|
|
|
1566
1568
|
}
|
|
1567
1569
|
|
|
1568
1570
|
const files = mentionedFiles(text);
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1571
|
+
// Multi-image: gom mọi ảnh paste (opts.images) + ảnh @file được nhắc tới.
|
|
1572
|
+
// Giữ thứ tự: ảnh paste trước, rồi ảnh @file. Dedupe theo data URL.
|
|
1573
|
+
const pasted = Array.isArray(opts.images) ? opts.images.filter(Boolean) : [];
|
|
1574
|
+
const mentionedImages = files
|
|
1575
|
+
.filter((f) => IMAGE_MIME[path.extname(f).toLowerCase()])
|
|
1576
|
+
.map((f) => imageDataUrl(f))
|
|
1577
|
+
.filter(Boolean);
|
|
1578
|
+
const images = [...new Set([...pasted, ...mentionedImages])];
|
|
1579
|
+
const image = images[0] || null;
|
|
1572
1580
|
const content = files.length
|
|
1573
1581
|
? text +
|
|
1574
|
-
`\n\n[File người dùng nhắc tới bằng @: ${files.join(', ')} — đọc bằng read_file nếu cần.${
|
|
1582
|
+
`\n\n[File người dùng nhắc tới bằng @: ${files.join(', ')} — đọc bằng read_file nếu cần.${images.length ? ` ${images.length} ảnh đã được đính kèm cho model vision.` : ''}]`
|
|
1575
1583
|
: text;
|
|
1576
1584
|
state.history.push({ role: 'user', content });
|
|
1577
1585
|
// Update terminal title với session name (trích từ message đầu).
|
|
@@ -1616,6 +1624,7 @@ NGUYÊN TẮC:
|
|
|
1616
1624
|
history: state.history,
|
|
1617
1625
|
model: state.model.id,
|
|
1618
1626
|
image,
|
|
1627
|
+
images,
|
|
1619
1628
|
signal: abort.signal,
|
|
1620
1629
|
tokenMeter,
|
|
1621
1630
|
goal: state.goal,
|
|
@@ -1926,152 +1935,32 @@ NGUYÊN TẮC:
|
|
|
1926
1935
|
break;
|
|
1927
1936
|
}
|
|
1928
1937
|
case 'kg': {
|
|
1929
|
-
// Knowledge graph CRUD —
|
|
1930
|
-
//
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
const [, name, etype, obsRaw] = m2;
|
|
1951
|
-
const observations = obsRaw
|
|
1952
|
-
? obsRaw
|
|
1953
|
-
.split(';')
|
|
1954
|
-
.map((s) => s.trim())
|
|
1955
|
-
.filter(Boolean)
|
|
1956
|
-
: [];
|
|
1957
|
-
const created = await kgCreateEntities([{ name, entityType: etype, observations }]);
|
|
1958
|
-
if (!created.length)
|
|
1959
|
-
console.log(c.dim(` Entity '${name}' đã tồn tại — không tạo lại`));
|
|
1960
|
-
else
|
|
1961
|
-
console.log(
|
|
1962
|
-
c.ok(' ✓ ') + `đã thêm ${name} [${etype}] (${observations.length} obs)`
|
|
1963
|
-
);
|
|
1964
|
-
}
|
|
1965
|
-
} else if (sub === 'obs') {
|
|
1966
|
-
const m2 = rest.match(/^(\S+)\s+([\s\S]+)$/);
|
|
1967
|
-
if (!m2) {
|
|
1968
|
-
console.log(c.err(' Cú pháp: /kg obs <Name> <observation text>'));
|
|
1969
|
-
} else {
|
|
1970
|
-
const [, name, obs] = m2;
|
|
1971
|
-
const out = await kgAddObservations([{ entityName: name, contents: [obs] }]);
|
|
1972
|
-
const added = out[0]?.addedObservations || [];
|
|
1973
|
-
if (!added.length)
|
|
1974
|
-
console.log(c.dim(` Observation đã có cho '${name}' — không thêm lại`));
|
|
1975
|
-
else console.log(c.ok(' ✓ ') + `đã thêm obs cho ${name}: ${added[0]}`);
|
|
1976
|
-
}
|
|
1977
|
-
} else if (sub === 'link') {
|
|
1978
|
-
const parts = rest.split(/\s+/).filter(Boolean);
|
|
1979
|
-
if (parts.length < 3) {
|
|
1980
|
-
console.log(c.err(' Cú pháp: /kg link <From> <verb> <To>'));
|
|
1981
|
-
} else {
|
|
1982
|
-
const [from, verb, ...toParts] = parts;
|
|
1983
|
-
const to = toParts.join(' ');
|
|
1984
|
-
const created = await kgCreateRelations([{ from, to, relationType: verb }]);
|
|
1985
|
-
if (!created.length) console.log(c.dim(' Relation đã tồn tại — không tạo lại'));
|
|
1986
|
-
else console.log(c.ok(' ✓ ') + `${from} --${verb}--> ${to}`);
|
|
1987
|
-
}
|
|
1988
|
-
} else if (sub === 'unlink') {
|
|
1989
|
-
const parts = rest.split(/\s+/).filter(Boolean);
|
|
1990
|
-
if (parts.length < 3) {
|
|
1991
|
-
console.log(c.err(' Cú pháp: /kg unlink <From> <verb> <To>'));
|
|
1992
|
-
} else {
|
|
1993
|
-
const [from, verb, ...toParts] = parts;
|
|
1994
|
-
const to = toParts.join(' ');
|
|
1995
|
-
const out = await kgDeleteRelations([{ from, to, relationType: verb }]);
|
|
1996
|
-
if (!out.deleted) console.log(c.dim(' Không có relation nào khớp'));
|
|
1997
|
-
else console.log(c.ok(' ✓ ') + `xóa relation ${from} --${verb}--> ${to}`);
|
|
1998
|
-
}
|
|
1999
|
-
} else if (sub === 'unobs') {
|
|
2000
|
-
const m2 = rest.match(/^(\S+)\s+([\s\S]+)$/);
|
|
2001
|
-
if (!m2) {
|
|
2002
|
-
console.log(c.err(' Cú pháp: /kg unobs <Name> <observation text>'));
|
|
2003
|
-
} else {
|
|
2004
|
-
const [, name, obs] = m2;
|
|
2005
|
-
await kgDeleteObservations([{ entityName: name, observations: [obs] }]);
|
|
2006
|
-
console.log(c.ok(' ✓ ') + `xóa obs '${obs}' khỏi ${name} (silent nếu không có)`);
|
|
2007
|
-
}
|
|
2008
|
-
} else if (sub === 'get') {
|
|
2009
|
-
if (!rest) {
|
|
2010
|
-
console.log(c.err(' Cú pháp: /kg get <Name1> [<Name2> ...]'));
|
|
2011
|
-
} else {
|
|
2012
|
-
const names = rest.split(/\s+/).filter(Boolean);
|
|
2013
|
-
const g = await kgOpenNodes(names);
|
|
2014
|
-
if (!g.entities.length)
|
|
2015
|
-
console.log(c.dim(` Không tìm thấy entity: ${names.join(', ')}`));
|
|
2016
|
-
else console.log(kgFormat(g));
|
|
2017
|
-
}
|
|
2018
|
-
} else if (sub === 'search') {
|
|
2019
|
-
if (!rest) {
|
|
2020
|
-
console.log(c.err(' Cú pháp: /kg search <query>'));
|
|
2021
|
-
} else {
|
|
2022
|
-
const g = await kgSearchNodes(rest);
|
|
2023
|
-
if (!g.entities.length) console.log(c.dim(` Không có kết quả cho "${rest}"`));
|
|
2024
|
-
else console.log(kgFormat(g));
|
|
2025
|
-
}
|
|
2026
|
-
} else if (sub === 'rm' || sub === 'remove' || sub === 'delete') {
|
|
2027
|
-
if (!rest) {
|
|
2028
|
-
console.log(c.err(' Cú pháp: /kg rm <Name1> [<Name2> ...]'));
|
|
2029
|
-
} else {
|
|
2030
|
-
const names = rest.split(/\s+/).filter(Boolean);
|
|
2031
|
-
const out = await kgDeleteEntities(names);
|
|
2032
|
-
console.log(c.ok(' ✓ ') + `đã xóa ${out.deleted} entity (cascade relation)`);
|
|
2033
|
-
}
|
|
2034
|
-
} else {
|
|
2035
|
-
console.log(
|
|
2036
|
-
c.err(' Sub-command không nhận diện. ') +
|
|
2037
|
-
c.dim('Dùng: list | path | add | obs | link | unlink | unobs | get | search | rm')
|
|
2038
|
-
);
|
|
2039
|
-
}
|
|
2040
|
-
} catch (e) {
|
|
2041
|
-
if (e instanceof KGEntityNotFound) {
|
|
2042
|
-
console.log(c.err(' ✗ ') + e.message + c.dim(' (gõ /kg add trước khi /kg obs)'));
|
|
2043
|
-
} else if (e instanceof KGMarkerError) {
|
|
2044
|
-
console.log(c.err(' ✗ ') + e.message);
|
|
2045
|
-
} else {
|
|
2046
|
-
console.log(c.err(' ✗ ') + (e?.message || String(e)));
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
1938
|
+
// Knowledge graph CRUD — logic tách sang src/repl/commands/kg.js.
|
|
1939
|
+
// Inject các hàm kg* đã import + helper màu c (không truy cập closure).
|
|
1940
|
+
await runKgCommand({
|
|
1941
|
+
arg,
|
|
1942
|
+
c,
|
|
1943
|
+
kg: {
|
|
1944
|
+
kgLoad,
|
|
1945
|
+
kgCreateEntities,
|
|
1946
|
+
kgCreateRelations,
|
|
1947
|
+
kgAddObservations,
|
|
1948
|
+
kgDeleteEntities,
|
|
1949
|
+
kgDeleteObservations,
|
|
1950
|
+
kgDeleteRelations,
|
|
1951
|
+
kgSearchNodes,
|
|
1952
|
+
kgOpenNodes,
|
|
1953
|
+
kgFormat,
|
|
1954
|
+
kgFilePath,
|
|
1955
|
+
KGEntityNotFound,
|
|
1956
|
+
KGMarkerError,
|
|
1957
|
+
},
|
|
1958
|
+
});
|
|
2049
1959
|
break;
|
|
2050
1960
|
}
|
|
2051
1961
|
case 'goal': {
|
|
2052
|
-
// HARD GOAL
|
|
2053
|
-
|
|
2054
|
-
const v = arg.trim();
|
|
2055
|
-
if (!v) {
|
|
2056
|
-
if (state.goal) console.log(c.accent(' 🎯 goal: ') + state.goal);
|
|
2057
|
-
else
|
|
2058
|
-
console.log(c.dim(' chưa đặt goal. Cú pháp: /goal <mục tiêu> · /goal clear để xoá'));
|
|
2059
|
-
} else if (
|
|
2060
|
-
v.toLowerCase() === 'clear' ||
|
|
2061
|
-
v.toLowerCase() === 'off' ||
|
|
2062
|
-
v.toLowerCase() === 'xoá' ||
|
|
2063
|
-
v.toLowerCase() === 'xoa'
|
|
2064
|
-
) {
|
|
2065
|
-
state.goal = null;
|
|
2066
|
-
tui.setGoal('');
|
|
2067
|
-
console.log(c.dim(' đã xoá goal'));
|
|
2068
|
-
persist();
|
|
2069
|
-
} else {
|
|
2070
|
-
state.goal = v;
|
|
2071
|
-
tui.setGoal(v);
|
|
2072
|
-
console.log(c.accent(' 🎯 đã đặt goal: ') + v);
|
|
2073
|
-
persist();
|
|
2074
|
-
}
|
|
1962
|
+
// HARD GOAL — logic tách sang src/repl/commands/goal.js. Inject deps.
|
|
1963
|
+
runGoalCommand({ arg, state, tui, persist, c });
|
|
2075
1964
|
break;
|
|
2076
1965
|
}
|
|
2077
1966
|
case 'tokens': {
|
package/src/tools.js
CHANGED
|
@@ -348,7 +348,17 @@ export const TOOLS = {
|
|
|
348
348
|
// raw:true hoặc không phải HTML → trả nguyên văn (đã clip). Ngược lại strip HTML.
|
|
349
349
|
const isHtml = /html/i.test(ct) || /^\s*<(!doctype|html)/i.test(body);
|
|
350
350
|
const text = raw || !isHtml ? body : htmlToText(body);
|
|
351
|
-
|
|
351
|
+
const trimmed = text.trim();
|
|
352
|
+
// SPA hint: HTML lớn nhưng strip ra gần rỗng → nhiều khả năng client-render (JS).
|
|
353
|
+
// htmlToText là regex, không chạy JS → trả signal rõ thay vì để model tưởng trang trống.
|
|
354
|
+
if (!raw && isHtml && trimmed.length < 200 && body.length > 2000) {
|
|
355
|
+
return clip(
|
|
356
|
+
head +
|
|
357
|
+
`[web_fetch: trang trả ${body.length} byte HTML nhưng chỉ strip được ${trimmed.length} ký tự text — nhiều khả năng render bằng JS (SPA). Thử lại với raw:true để xem HTML thô, hoặc tìm API/endpoint dữ liệu của trang.]\n\n` +
|
|
358
|
+
trimmed
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return clip(head + trimmed);
|
|
352
362
|
},
|
|
353
363
|
|
|
354
364
|
async write_file({ path: p, content }, { signal } = {}) {
|
|
@@ -408,7 +418,7 @@ export const TOOLS = {
|
|
|
408
418
|
// Không thấy → lỗi GIÀU THÔNG TIN: cho model thấy đúng byte trong file để sửa.
|
|
409
419
|
throw new Error(
|
|
410
420
|
`old_string not found in ${rel(file)}. Copy the target text EXACTLY (indentation/whitespace included, NO line-number prefix).` +
|
|
411
|
-
nearbyContext(data, old_string)
|
|
421
|
+
nearbyContext(data, old_string, rel(file))
|
|
412
422
|
);
|
|
413
423
|
},
|
|
414
424
|
|
|
@@ -730,7 +740,8 @@ function matchByLines(data, oldStr) {
|
|
|
730
740
|
|
|
731
741
|
// Khi không khớp: in vùng file gần dòng giống nhất, dạng JSON-escaped để model
|
|
732
742
|
// thấy rõ tab/space → sửa old_string cho khớp ngay lần sau.
|
|
733
|
-
function nearbyContext(data, oldStr) {
|
|
743
|
+
function nearbyContext(data, oldStr, p) {
|
|
744
|
+
const oldLineCount = oldStr.replace(/\r\n/g, '\n').split('\n').length;
|
|
734
745
|
const want = (
|
|
735
746
|
oldStr
|
|
736
747
|
.replace(/\r\n/g, '\n')
|
|
@@ -756,14 +767,19 @@ function nearbyContext(data, oldStr) {
|
|
|
756
767
|
}
|
|
757
768
|
}
|
|
758
769
|
if (hit < 0 || best < 6)
|
|
759
|
-
return ` (no similar line found; the file has ${lines.length} lines — re-read it.)`;
|
|
770
|
+
return ` (no similar line found; the file has ${lines.length} lines — re-read it.)${readFileRetryHint(p, 1, Math.min(lines.length, 80))}`;
|
|
760
771
|
const a = Math.max(0, hit - 2);
|
|
761
|
-
const b = Math.min(lines.length, hit +
|
|
772
|
+
const b = Math.min(lines.length, hit + Math.max(8, oldLineCount + 5));
|
|
762
773
|
const snippet = lines
|
|
763
774
|
.slice(a, b)
|
|
764
775
|
.map((l, k) => ` ${a + k + 1}: ${JSON.stringify(l)}`)
|
|
765
776
|
.join('\n');
|
|
766
|
-
return `\nActual file lines near the closest match (JSON-escaped — match these bytes EXACTLY):\n${snippet}`;
|
|
777
|
+
return `\nActual file lines near the closest match (JSON-escaped — match these bytes EXACTLY):\n${snippet}${readFileRetryHint(p, a + 1, b - a)}`;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function readFileRetryHint(pathHint, offset, limit) {
|
|
781
|
+
const pathPart = `"path":${JSON.stringify(pathHint || '<same path>')},`;
|
|
782
|
+
return `\nNEXT REQUIRED TOOL: read_file {${pathPart}"offset":${offset},"limit":${limit}}. Then retry edit_file using text copied from that fresh read_file result.`;
|
|
767
783
|
}
|
|
768
784
|
|
|
769
785
|
function globToRegExp(glob) {
|