mocode-ai 0.1.4 → 0.1.6

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.
Files changed (59) hide show
  1. package/README.md +196 -188
  2. package/dist/agent/core.js +309 -0
  3. package/dist/agent/index.js +62 -249
  4. package/dist/agent/mode.js +46 -0
  5. package/dist/agent/spawn.js +123 -0
  6. package/dist/commands/config.js +8 -26
  7. package/dist/config/file.js +48 -0
  8. package/dist/config/index.js +81 -36
  9. package/dist/context/classifier.js +83 -0
  10. package/dist/context/encoders/index.js +10 -0
  11. package/dist/context/encoders/passthrough.js +23 -0
  12. package/dist/context/index.js +10 -0
  13. package/dist/context/pipeline.js +84 -0
  14. package/dist/context/registry.js +30 -0
  15. package/dist/context/types.js +12 -0
  16. package/dist/index.js +12 -2
  17. package/dist/llm/index.js +11 -9
  18. package/dist/memory/index.js +2 -2
  19. package/dist/memory/reflect.js +11 -11
  20. package/dist/memory/store.js +4 -4
  21. package/dist/repl/index.js +269 -109
  22. package/dist/sandbox/command.js +41 -0
  23. package/dist/sandbox/index.js +5 -0
  24. package/dist/sandbox/jail.js +75 -0
  25. package/dist/sandbox/policy.js +58 -0
  26. package/dist/sandbox/root.js +19 -0
  27. package/dist/session/compact.js +1 -1
  28. package/dist/skills/index.js +2 -2
  29. package/dist/tools/builtins/ask-human.js +7 -7
  30. package/dist/tools/builtins/codegraph.js +113 -0
  31. package/dist/tools/builtins/edit-file.js +3 -3
  32. package/dist/tools/builtins/glob.js +10 -5
  33. package/dist/tools/builtins/grep.js +14 -9
  34. package/dist/tools/builtins/index.js +7 -1
  35. package/dist/tools/builtins/memory-forget.js +2 -2
  36. package/dist/tools/builtins/memory-list.js +2 -2
  37. package/dist/tools/builtins/memory-save.js +7 -7
  38. package/dist/tools/builtins/memory-search.js +4 -4
  39. package/dist/tools/builtins/memory-update.js +6 -6
  40. package/dist/tools/builtins/read-file.js +5 -4
  41. package/dist/tools/builtins/run-command.js +47 -8
  42. package/dist/tools/builtins/switch-mode.js +46 -0
  43. package/dist/tools/builtins/task.js +62 -0
  44. package/dist/tools/builtins/use-skill.js +2 -2
  45. package/dist/tools/builtins/web-fetch.js +16 -3
  46. package/dist/tools/builtins/web-search.js +6 -6
  47. package/dist/tools/builtins/write-file.js +3 -3
  48. package/dist/tools/constants.js +16 -0
  49. package/dist/tools/registry.js +18 -4
  50. package/dist/ui/content.js +25 -17
  51. package/dist/ui/diff.js +28 -26
  52. package/dist/ui/intervention.js +24 -3
  53. package/dist/ui/layout.js +295 -81
  54. package/dist/ui/markdown.js +607 -0
  55. package/dist/ui/mouse.js +93 -0
  56. package/dist/ui/prompt.js +259 -5
  57. package/dist/ui/render.js +1 -1
  58. package/dist/ui/theme.js +146 -13
  59. package/package.json +2 -2
package/dist/ui/prompt.js CHANGED
@@ -3,6 +3,7 @@ import { stdin, stdout } from 'node:process';
3
3
  import { ui } from './theme.js';
4
4
  import { displayWidth, padEndDisplay, truncateDisplay } from './render.js';
5
5
  import * as layout from './layout.js';
6
+ import * as mouse from './mouse.js';
6
7
  // ── 粘贴检测(块级 + 时间窗)──
7
8
  // 块级:多字节大块(len>8)或含 CR/LF 的小块 = 粘贴(键盘单键 1 字节,Enter=\r 单字节)。
8
9
  // 时间窗:每块重置 50ms 计时器,静默 50ms 即"粘贴结束"→ onPasteEnd。跨多块的大粘贴(块间 <50ms)累积进
@@ -18,7 +19,10 @@ function ensurePasteDetector() {
18
19
  return;
19
20
  pasteDetectorInstalled = true;
20
21
  stdin.on('data', (chunk) => {
21
- const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
22
+ const raw = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
23
+ // 剔除 SGR 鼠标报告(\x1B[<…M/m,连半截也匹配):防快速滚轮/触控板动量在一个 chunk 里拼 ≥2 条
24
+ // 报表(22 字符 > 16 阈值)误判粘贴 50ms、把后续真按键泄进 pasteParts。
25
+ const text = raw.replace(/\x1b\[<[0-9;]*[Mm]/g, '');
22
26
  const hasNL = text.indexOf('\r') >= 0 || text.indexOf('\n') >= 0;
23
27
  // 按字符数(码点)判粘贴,非字节:CJK 汉字占 3 UTF-8 字节,旧阈值(len>8 字节)会把 IME 提交的
24
28
  // 3-10 个汉字误判为粘贴 → 进 50ms 缓冲 → finalizePaste→insertText 落字(且旧 insertText 把光标
@@ -261,6 +265,22 @@ export async function promptWithSlashMenu(opts) {
261
265
  function onKey(_str, key) {
262
266
  if (resolved || !key)
263
267
  return;
268
+ // SGR 鼠标滚轮:readline 把 \x1B[<btn;col;rowM 拆成多 fragment,经 mouse.consumeMouse 重组;
269
+ // 滚轮 → scrollBy(±5)(只重画内容区,底栏不动)。置于 pasting 之前——鼠标 fragment 永不进 pasteParts。
270
+ const _m = mouse.consumeMouse(key.sequence ?? '');
271
+ if (_m.suppress) {
272
+ if (_m.wheel)
273
+ layout.scrollBy(_m.wheel * 5);
274
+ return;
275
+ }
276
+ // Shift+Tab:循环切换 agent 模式(auto ↔ plan)。不插字符、不提交、不影响输入文本;
277
+ // 回调由 repl 注入(翻 agentMode + 重写 history[0] + 设状态行 modeTag),再 redraw() 经
278
+ // paintInput 重画底栏(状态行 chip 即时刷新 + 光标留输入框)。置于 case 'tab' 之前,故不触发菜单补全。
279
+ if (key.shift && key.name === 'tab') {
280
+ opts.onCycleMode?.();
281
+ redraw();
282
+ return;
283
+ }
264
284
  // Ctrl+C:有内容则清空,再按一次(空)才退出——置顶,使粘贴中也能被截到(否则被 pasting 分支吞掉)
265
285
  if (key.ctrl && key.name === 'c') {
266
286
  onCtrlC();
@@ -284,9 +304,9 @@ export async function promptWithSlashMenu(opts) {
284
304
  pasteParts.push(s);
285
305
  return;
286
306
  }
287
- // 滚动回看键(优先;不触发回尾):PgUp/PgDn 翻页,Ctrl+↑↓ 与 plain ↑/↓ 单行。
307
+ // 滚动回看键(优先;不触发回尾):PgUp/PgDn 翻页,Ctrl+↑↓ 与 plain ↑/↓ 每次 5 行。
288
308
  // plain ↑/↓ 仅在单行输入且菜单关闭时作滚动(多行编辑留给光标移动,菜单打开留给选项);
289
- // 兼鼠标滚轮——WT alt 屏(经 \x1B[?1007h)滚轮转发 ↑/↓。
309
+ // 鼠标滚轮已由 onKey 顶部 SGR 重组守卫处理(不经此分支);此分支仅键盘 plain ↑/↓,放大到 5 行/格。
290
310
  const plainArrowScroll = (key.name === 'up' || key.name === 'down') &&
291
311
  !key.ctrl &&
292
312
  !key.meta &&
@@ -303,9 +323,9 @@ export async function promptWithSlashMenu(opts) {
303
323
  else if (key.name === 'pagedown')
304
324
  layout.scrollBy(-pageH);
305
325
  else if (key.name === 'up')
306
- layout.scrollBy(1);
326
+ layout.scrollBy(5);
307
327
  else
308
- layout.scrollBy(-1);
328
+ layout.scrollBy(-5);
309
329
  return;
310
330
  }
311
331
  // 其他键:若处于滚动回看,先回尾再处理(打字即回底)
@@ -729,3 +749,237 @@ export async function promptSessionPicker(items, recentCap = 10) {
729
749
  redraw();
730
750
  });
731
751
  }
752
+ /**
753
+ * 主题选择菜单(供 /theme):↑/↓ 导航、Enter 切换、Esc/Ctrl+D 取消、Ctrl+C 抛 SIGINT(调用方 try/catch)。
754
+ * 复用 SessionPickerItem 形状({id,title,subtitle?});选中项 cyan+bold + ▸ 高亮(同 /resume 菜单)。
755
+ * 返回选中的 item(含 id);null=取消 / 非 TTY / 空列表。纯导航(不收文本输入)。
756
+ * 长列表超屏高自动开窗保光标可见;默认聚焦首项。
757
+ */
758
+ export async function promptThemePicker(items) {
759
+ if (!layout.isActive() || items.length === 0)
760
+ return null;
761
+ const emitter = stdin;
762
+ let selected = 0;
763
+ let resolved = false;
764
+ let resolve;
765
+ let reject;
766
+ const hint = '↑↓ 选择 · Enter 切换 · Esc 取消';
767
+ /** 菜单行(带开窗):超屏高时以 selected 为中心取窗,保光标可见。行格式:▸ N title subtitle。 */
768
+ function menuLines() {
769
+ const g = layout.getGeo();
770
+ const cols = g.cols;
771
+ const maxRows = Math.max(1, g.contentBottom);
772
+ let start = 0;
773
+ if (items.length > maxRows) {
774
+ start = Math.max(0, Math.min(selected - Math.floor(maxRows / 2), items.length - maxRows));
775
+ }
776
+ const count = Math.min(maxRows, items.length);
777
+ return Array.from({ length: count }, (_, i) => {
778
+ const idx = start + i;
779
+ const isSel = idx === selected;
780
+ const color = isSel ? `${ui.cyan}${ui.bold}` : ui.dim;
781
+ const marker = isSel ? `${ui.cyan}${ui.bold}▸${ui.reset}` : ' ';
782
+ const num = String(idx + 1);
783
+ const it = items[idx];
784
+ const title = it.title || '(无)';
785
+ const sub = it.subtitle ?? '';
786
+ const leadW = displayWidth(num) + 4; // "▸ " + num + " "
787
+ let subW = sub ? displayWidth(sub) + 2 : 0; // " " + sub
788
+ let titleW = cols - leadW - subW;
789
+ if (titleW < 4 && sub) {
790
+ subW = 0;
791
+ titleW = cols - leadW;
792
+ }
793
+ const titleT = titleW > 0 ? truncateDisplay(title, titleW) : '';
794
+ const subPart = subW > 0 ? ` ${sub}` : '';
795
+ return `${marker} ${color}${num} ${titleT}${subPart}${ui.reset}`;
796
+ });
797
+ }
798
+ function redraw() {
799
+ layout.paintInput({
800
+ prompt: '❯ ',
801
+ lines: [hint],
802
+ cursorLine: 0,
803
+ cursorCol: displayWidth(hint),
804
+ menu: { lines: menuLines() },
805
+ caret: false, // 纯导航菜单:不画输入框块状光标,聚焦由菜单 ▸ 标记
806
+ });
807
+ }
808
+ function cleanup() {
809
+ try {
810
+ stdin.setRawMode(false);
811
+ }
812
+ catch {
813
+ // 忽略
814
+ }
815
+ emitter.removeListener('keypress', onKey);
816
+ stdin.pause();
817
+ }
818
+ function finish(value) {
819
+ if (resolved)
820
+ return;
821
+ resolved = true;
822
+ cleanup();
823
+ resolve(value);
824
+ }
825
+ function onKey(_str, key) {
826
+ if (resolved || !key)
827
+ return;
828
+ if (key.ctrl && key.name === 'c') {
829
+ cleanup();
830
+ reject(new Error('SIGINT'));
831
+ return;
832
+ }
833
+ if (key.ctrl && key.name === 'd') {
834
+ finish(null);
835
+ return;
836
+ }
837
+ const n = items.length;
838
+ switch (key.name) {
839
+ case 'up':
840
+ selected = (selected - 1 + n) % n;
841
+ redraw();
842
+ return;
843
+ case 'down':
844
+ selected = (selected + 1) % n;
845
+ redraw();
846
+ return;
847
+ case 'return':
848
+ case 'enter':
849
+ finish(items[selected] ?? null);
850
+ return;
851
+ case 'escape':
852
+ finish(null);
853
+ return;
854
+ }
855
+ }
856
+ return new Promise((res, rej) => {
857
+ resolve = res;
858
+ reject = rej;
859
+ ensurePasteDetector();
860
+ readline.emitKeypressEvents(stdin);
861
+ let rawOk = true;
862
+ try {
863
+ stdin.setRawMode(true);
864
+ }
865
+ catch {
866
+ rawOk = false;
867
+ }
868
+ if (!rawOk) {
869
+ res(null);
870
+ return;
871
+ }
872
+ stdin.resume();
873
+ emitter.on('keypress', onKey);
874
+ redraw();
875
+ });
876
+ }
877
+ /**
878
+ * 回滚方式二选一菜单(供 /rollback 选完轮次后):↑/↓ 导航、Enter 确认、Esc/Ctrl+D 保留文件。
879
+ * 两个选项:1=撤销文件改动(恢复到回滚前);2=只撤销消息,保留文件改动。
880
+ * 复用轮次/会话菜单的渲染骨架(raw mode + layout.paintInput 的 menu),选中项 cyan+bold + ▸ 高亮。
881
+ * 返回 true=撤销文件 / false=只撤销消息 / null=取消(调用方按 false=保留文件处理)。
882
+ * 默认聚焦首项(撤销文件)。无文件改动时调用方不应调此菜单(直接走只撤销消息)。
883
+ */
884
+ export async function promptRevertChoice(fileCount) {
885
+ if (!layout.isActive())
886
+ return null;
887
+ const emitter = stdin;
888
+ const hint = '↑↓ 选择 · Enter 确认 · Esc 保留文件';
889
+ const items = [
890
+ `撤销文件改动${fileCount > 0 ? `(${fileCount} 个文件恢复到回滚前)` : ''}`,
891
+ '只撤销消息,保留文件改动',
892
+ ];
893
+ let selected = 0; // 默认聚焦首项(撤销文件)
894
+ let resolved = false;
895
+ let resolve;
896
+ let reject;
897
+ function menuLines() {
898
+ const cols = layout.getGeo().cols;
899
+ return items.map((text, idx) => {
900
+ const isSel = idx === selected;
901
+ const color = isSel ? `${ui.cyan}${ui.bold}` : ui.dim;
902
+ const marker = isSel ? `${ui.cyan}${ui.bold}▸${ui.reset}` : ' ';
903
+ const num = `${color}${idx + 1}${ui.reset}`;
904
+ const t = truncateDisplay(text, cols - 6);
905
+ return `${marker} ${num} ${color}${t}${ui.reset}`;
906
+ });
907
+ }
908
+ function redraw() {
909
+ layout.paintInput({
910
+ prompt: '❯ ',
911
+ lines: [hint],
912
+ cursorLine: 0,
913
+ cursorCol: displayWidth(hint),
914
+ menu: { lines: menuLines() },
915
+ caret: false, // 纯导航菜单:不画输入框块状光标,聚焦由菜单 ▸ 标记
916
+ });
917
+ }
918
+ function cleanup() {
919
+ try {
920
+ stdin.setRawMode(false);
921
+ }
922
+ catch {
923
+ // 忽略
924
+ }
925
+ emitter.removeListener('keypress', onKey);
926
+ stdin.pause();
927
+ }
928
+ function finish(value) {
929
+ if (resolved)
930
+ return;
931
+ resolved = true;
932
+ cleanup();
933
+ resolve(value);
934
+ }
935
+ function onKey(_str, key) {
936
+ if (resolved || !key)
937
+ return;
938
+ if (key.ctrl && key.name === 'c') {
939
+ cleanup();
940
+ reject(new Error('SIGINT'));
941
+ return;
942
+ }
943
+ if (key.ctrl && key.name === 'd') {
944
+ finish(null);
945
+ return;
946
+ }
947
+ switch (key.name) {
948
+ case 'up':
949
+ selected = (selected - 1 + items.length) % items.length;
950
+ redraw();
951
+ return;
952
+ case 'down':
953
+ selected = (selected + 1) % items.length;
954
+ redraw();
955
+ return;
956
+ case 'return':
957
+ case 'enter':
958
+ finish(selected === 0); // true=撤销文件, false=只撤销消息
959
+ return;
960
+ case 'escape':
961
+ finish(null); // 取消=保留文件
962
+ return;
963
+ }
964
+ }
965
+ return new Promise((res, rej) => {
966
+ resolve = res;
967
+ reject = rej;
968
+ ensurePasteDetector();
969
+ readline.emitKeypressEvents(stdin);
970
+ let rawOk = true;
971
+ try {
972
+ stdin.setRawMode(true);
973
+ }
974
+ catch {
975
+ rawOk = false;
976
+ }
977
+ if (!rawOk) {
978
+ res(null);
979
+ return;
980
+ }
981
+ stdin.resume();
982
+ emitter.on('keypress', onKey);
983
+ redraw();
984
+ });
985
+ }
package/dist/ui/render.js CHANGED
@@ -194,7 +194,7 @@ function labelContent(label, value) {
194
194
  /** 横幅纯文本(带 ANSI 颜色,不写出)——供 TUI 经 contentWrite 写入内容区以跟踪续写位。
195
195
  * 无边框:左侧实心小熊 + 右侧标题/信息(模型/目录),末尾一行提示。 */
196
196
  export function bannerString(info) {
197
- const title = `${ui.bold}${ui.brightCyan}◆ mocode${ui.reset} ${ui.dim}v${VERSION}${ui.reset}`;
197
+ const title = `${ui.bold}${ui.brightCyan}◆ MoCode${ui.reset} ${ui.dim}v${VERSION}${ui.reset}`;
198
198
  const rows = [
199
199
  logoLine(0) + title,
200
200
  logoLine(1) + labelContent('模型', info.model),
package/dist/ui/theme.js CHANGED
@@ -1,25 +1,158 @@
1
1
  import { stdout } from 'node:process';
2
2
  /**
3
- * 终端 UI 主题:TTY 感知的 ANSI 颜色。
4
- * 非 TTY(管道 / 重定向)时颜色退化为空串,避免把转义码原样打到日志里。
3
+ * 终端 UI 主题:TTY 感知的 ANSI 颜色 + 多套可切换调色板。
4
+ *
5
+ * 设计:
6
+ * - `ui` 是单例对象,颜色字段为 **getter**——每次 `${ui.cyan}` 都现取当前调色板的值,
7
+ * 故 `setTheme(name)` 后全项目 60+ 内联调用点零改动即自动跟随。`isTTY`/`reset`/
8
+ * `bold`/`dim`/`reverse` 是属性 / 跨主题不变量,保持普通字面量(非 getter)。
9
+ * - 非 TTY(管道 / 重定向)时 `wrap` 把所有颜色退化成空串,避免转义码污染日志;此门控
10
+ * 集中在 getter 里,是正确性不变量——切主题不会绕过它。
11
+ * - `getThemeVersion()` 在 `setTheme` 时自增,供 markdown MEMO 等缓存按版本失效
12
+ * (否则切主题后重渲染会命中旧主题的缓存行)。
13
+ * - truecolor(`\x1B[38;2;r;g;bm`)安全:渲染层 SGR 正则 `/\x1b\[[0-9;]*m/` 通配;
14
+ * 调色板约定所有 span 用 `ui.reset`(`\x1B[0m`)闭合(content.ts 只认精确 reset)。
15
+ *
16
+ * 叶子模块:不反向依赖 config / 业务。启动时由 repl 调 `setTheme(config.theme)` 注入。
5
17
  */
6
18
  const isTTY = Boolean(stdout.isTTY);
7
19
  const wrap = (code) => (isTTY ? code : '');
20
+ /** default = One Dark(truecolor):平衡柔和的深色调色板,跨终端一致(不再依赖终端 16 色板)。 */
21
+ const DEFAULT = {
22
+ red: '\x1B[38;2;224;108;117m',
23
+ green: '\x1B[38;2;152;195;121m',
24
+ yellow: '\x1B[38;2;229;192;123m',
25
+ blue: '\x1B[38;2;97;175;239m',
26
+ magenta: '\x1B[38;2;198;120;221m',
27
+ cyan: '\x1B[38;2;86;182;194m',
28
+ gray: '\x1B[38;2;92;99;112m',
29
+ brightCyan: '\x1B[38;2;86;182;194m',
30
+ brightMagenta: '\x1B[38;2;198;120;221m',
31
+ userBg: '\x1B[48;2;44;49;60m', // One Dark current-line bg,深色终端上微妙可辨
32
+ };
33
+ /**
34
+ * 内建主题表。default=One Dark、light=Solarized Light、solarized=Solarized Dark、
35
+ * gruvbox / nord 取各自经典调色板的 truecolor 强调色。值可调;关键是不存在悬空 reset(用 \x1B[0m 闭合)。
36
+ */
37
+ const THEMES = {
38
+ default: DEFAULT,
39
+ light: {
40
+ // Solarized Light:浅底(base3 #fdf6e3)用 Solarized 经典强调色(与 solarized 暗底同源);
41
+ // gray 取 base01(浅底下可读的次级灰),userBg 取 base2(浅米底)。
42
+ red: '\x1B[38;2;220;50;47m',
43
+ green: '\x1B[38;2;133;153;0m',
44
+ yellow: '\x1B[38;2;181;137;0m',
45
+ blue: '\x1B[38;2;38;139;210m',
46
+ magenta: '\x1B[38;2;211;54;130m',
47
+ cyan: '\x1B[38;2;42;161;152m',
48
+ gray: '\x1B[38;2;88;110;117m',
49
+ brightCyan: '\x1B[38;2;42;161;152m',
50
+ brightMagenta: '\x1B[38;2;108;113;196m',
51
+ userBg: '\x1B[48;2;238;232;213m',
52
+ },
53
+ solarized: {
54
+ red: '\x1B[38;2;220;50;47m',
55
+ green: '\x1B[38;2;133;153;0m',
56
+ yellow: '\x1B[38;2;181;137;0m',
57
+ blue: '\x1B[38;2;38;139;210m',
58
+ magenta: '\x1B[38;2;211;54;130m',
59
+ cyan: '\x1B[38;2;42;161;152m',
60
+ gray: '\x1B[38;2;88;110;117m',
61
+ brightCyan: '\x1B[38;2;147;161;161m',
62
+ brightMagenta: '\x1B[38;2;108;113;196m',
63
+ userBg: '\x1B[48;2;7;54;66m',
64
+ },
65
+ gruvbox: {
66
+ red: '\x1B[38;2;251;73;52m',
67
+ green: '\x1B[38;2;184;187;38m',
68
+ yellow: '\x1B[38;2;250;189;47m',
69
+ blue: '\x1B[38;2;131;165;152m',
70
+ magenta: '\x1B[38;2;211;134;155m',
71
+ cyan: '\x1B[38;2;142;192;124m',
72
+ gray: '\x1B[38;2;146;131;116m',
73
+ brightCyan: '\x1B[38;2;142;192;124m',
74
+ brightMagenta: '\x1B[38;2;211;134;155m',
75
+ userBg: '\x1B[48;2;40;40;40m',
76
+ },
77
+ nord: {
78
+ red: '\x1B[38;2;191;97;106m',
79
+ green: '\x1B[38;2;163;190;140m',
80
+ yellow: '\x1B[38;2;235;203;139m',
81
+ blue: '\x1B[38;2;129;161;193m',
82
+ magenta: '\x1B[38;2;180;142;173m',
83
+ cyan: '\x1B[38;2;143;188;187m',
84
+ gray: '\x1B[38;2;76;86;106m',
85
+ brightCyan: '\x1B[38;2;136;192;208m',
86
+ brightMagenta: '\x1B[38;2;180;142;173m',
87
+ userBg: '\x1B[48;2;67;76;94m',
88
+ },
89
+ };
90
+ let currentName = 'default';
91
+ let version = 0;
92
+ function currentPalette() {
93
+ return THEMES[currentName] ?? DEFAULT;
94
+ }
95
+ /** 取某颜色字段的当前 ANSI 码(经 isTTY 门控)。 */
96
+ function color(key) {
97
+ return wrap(currentPalette()[key] ?? DEFAULT[key]);
98
+ }
99
+ /**
100
+ * 切主题。name 未知则回退 default(不抛——启动时 config.theme 可能是用户填的任意值)。
101
+ * 仅当实际变更时自增 version(供缓存失效)。 */
102
+ export function setTheme(name) {
103
+ const next = THEMES[name] ? name : 'default';
104
+ if (currentName !== next) {
105
+ currentName = next;
106
+ version++;
107
+ }
108
+ }
109
+ export function getTheme() {
110
+ return currentName;
111
+ }
112
+ export function listThemes() {
113
+ return Object.keys(THEMES);
114
+ }
115
+ export function themeExists(name) {
116
+ return name in THEMES;
117
+ }
118
+ /** 主题版本号,每次 setTheme 实际变更自增;供 markdown MEMO 等缓存按版本失效。 */
119
+ export function getThemeVersion() {
120
+ return version;
121
+ }
8
122
  export const ui = {
9
123
  isTTY,
10
124
  reset: wrap('\x1B[0m'),
11
125
  bold: wrap('\x1B[1m'),
12
126
  dim: wrap('\x1B[2m'),
13
127
  reverse: wrap('\x1B[7m'), // 反白(fg/bg 互换):块状输入光标用,光标处字符整格反白
14
- red: wrap('\x1B[31m'),
15
- green: wrap('\x1B[32m'),
16
- yellow: wrap('\x1B[33m'),
17
- blue: wrap('\x1B[34m'),
18
- cyan: wrap('\x1B[36m'),
19
- gray: wrap('\x1B[90m'),
20
- magenta: wrap('\x1B[35m'),
21
- brightCyan: wrap('\x1B[96m'),
22
- brightMagenta: wrap('\x1B[95m'),
23
- /** 用户消息满宽背景色(上滑时易辨认);bright black bg = 深灰,深色终端上微妙可辨。 */
24
- userBg: wrap('\x1B[100m'),
128
+ get red() {
129
+ return color('red');
130
+ },
131
+ get green() {
132
+ return color('green');
133
+ },
134
+ get yellow() {
135
+ return color('yellow');
136
+ },
137
+ get blue() {
138
+ return color('blue');
139
+ },
140
+ get magenta() {
141
+ return color('magenta');
142
+ },
143
+ get cyan() {
144
+ return color('cyan');
145
+ },
146
+ get gray() {
147
+ return color('gray');
148
+ },
149
+ get brightCyan() {
150
+ return color('brightCyan');
151
+ },
152
+ get brightMagenta() {
153
+ return color('brightMagenta');
154
+ },
155
+ get userBg() {
156
+ return color('userBg');
157
+ },
25
158
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "0.1.4",
4
- "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 9 个工具,接任意 OpenAI 兼容后端。",
3
+ "version": "0.1.6",
4
+ "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mocode": "bin/mocode.js"