@noobdemon/noob-cli 1.13.1 → 1.13.3
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 +22 -0
- package/README.md +139 -106
- package/package.json +3 -2
- package/src/agent.js +9 -5
- 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 +11 -1
- package/src/tui.js +128 -29
|
@@ -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 } = {}) {
|