@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.
@@ -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
- const image =
1570
- opts.images?.[0] ||
1571
- imageDataUrl(files.find((f) => IMAGE_MIME[path.extname(f).toLowerCase()]));
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.${image ? ' Ảnh đầu tiên đã được đính kèm cho model vision.' : ''}]`
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 — port từ mcp-knowledge-graph, lưu .noob/kg.jsonl.
1930
- // Sub-cmd: list, path, add, obs, link, unlink, unobs, get, search, rm.
1931
- const argText = String(arg || '').trim();
1932
- const m = argText.match(/^(\S+)(?:\s+([\s\S]*))?$/);
1933
- const sub = m ? m[1].toLowerCase() : '';
1934
- const rest = m && m[2] ? m[2].trim() : '';
1935
- try {
1936
- if (!sub || sub === 'list' || sub === 'ls') {
1937
- const g = await kgLoad();
1938
- if (!g.entities.length && !g.relations.length) {
1939
- console.log(c.dim(` KG rỗng (${kgFilePath()})`));
1940
- } else {
1941
- console.log(kgFormat(g));
1942
- }
1943
- } else if (sub === 'path') {
1944
- console.log(c.dim(' ') + kgFilePath());
1945
- } else if (sub === 'add') {
1946
- const m2 = rest.match(/^(\S+)\s+(\S+)(?:\s+([\s\S]+))?$/);
1947
- if (!m2) {
1948
- console.log(c.err(' Cú pháp: /kg add <Name> <type> [<obs1>; <obs2>; ...]'));
1949
- } else {
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 = completion requirement (xem tweet_dump.txt mục "Combine
2053
- // with /goal and /loop"). Set xong sẽ inject vào MỌI prompt tới khi clear.
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
- return clip(head + text.trim());
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 } = {}) {