deel-local-cli 1.1.0 → 1.2.0

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 (43) hide show
  1. package/README.en.md +347 -9
  2. package/README.md +317 -6
  3. package/package.json +2 -2
  4. package/src/agent/budget.js +0 -14
  5. package/src/agent/compact.js +87 -0
  6. package/src/agent/evolve.js +213 -0
  7. package/src/agent/loop.js +38 -1
  8. package/src/agent/modes.js +2 -0
  9. package/src/agent/route.js +55 -3
  10. package/src/agent/session.js +145 -1
  11. package/src/agent/threads.js +154 -0
  12. package/src/backend/http.js +41 -4
  13. package/src/commands.js +268 -4
  14. package/src/config.js +10 -1
  15. package/src/oneshot.js +4 -0
  16. package/src/preview/serve.js +326 -0
  17. package/src/repl.js +210 -34
  18. package/src/safety/guard.js +37 -2
  19. package/src/safety/undo.js +1 -1
  20. package/src/setup.js +2 -2
  21. package/src/skills/builtin//352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md +64 -0
  22. package/src/skills/builtin//352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md +78 -0
  23. package/src/skills/builtin//353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md +65 -0
  24. package/src/skills/builtin//354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md +74 -0
  25. package/src/skills/builtin//354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md +59 -0
  26. package/src/skills/builtin//354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md +73 -0
  27. package/src/skills/builtin//354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md +66 -0
  28. package/src/skills/discover.js +18 -3
  29. package/src/tools/edit-match.js +16 -5
  30. package/src/tools/excel-com.js +1 -1
  31. package/src/tools/excel.js +1 -1
  32. package/src/tools/fsutil.js +0 -8
  33. package/src/tools/index.js +1 -1
  34. package/src/tools/todo.js +19 -3
  35. package/src/ui/ansi.js +6 -2
  36. package/src/ui/inputbox.js +37 -8
  37. package/src/ui/md.js +227 -0
  38. package/src/ui/motion.js +212 -0
  39. package/src/ui/prompt.js +1 -1
  40. package/src/ui/screen.js +1 -1
  41. package/src/ui/status.js +14 -1
  42. package/src/ui/working.js +0 -3
  43. package/src/ui/wrap.js +1 -8
package/src/commands.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // 슬래시 명령. 이름은 Claude Code / Codex 관례에 맞춘다.
2
- import { writeFileSync, existsSync } from 'node:fs';
2
+ import { writeFileSync, existsSync, statSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { c, say, rule, pad, bar, mark, width, clip } from './ui/ansi.js';
5
5
  import { compact } from './agent/compact.js';
@@ -17,6 +17,7 @@ import { MODES as WORK_MODES, ORDER as WORK_ORDER, DEFAULT as WORK_DEFAULT, norm
17
17
  import { LEVELS, ORDER as LEVEL_ORDER, DEFAULT as LEVEL_DEFAULT, normalize as normLevel, shows as levelShows } from './ui/level.js';
18
18
  import { diffLines, renderDiff, shortStat } from './ui/diff.js';
19
19
  import { readTextFull } from './tools/fsutil.js';
20
+ import { 띄우기, 브라우저로 } from './preview/serve.js';
20
21
  const MODES = {
21
22
  auto: '자율 — 전부 알아서. 되돌리기가 안전망',
22
23
  confirm: '확인 — 되돌릴 수 없는 것만 물어봄',
@@ -46,12 +47,15 @@ export const COMMANDS = {
46
47
  orchestrator: { desc: '작업 모드 → 총괄 (큰 일을 쪼개서)' },
47
48
  undo: { desc: '직전 작업 되돌리기', arg: '[턴수]' },
48
49
  diff: { desc: '이번 대화에서 바뀐 파일 · 바뀐 자리 보기', arg: '[파일]' },
50
+ preview: { desc: '만든 웹을 이 자리에서 띄워 본다 — 올릴 필요 없음', arg: '[폴더|off]' },
49
51
  tools: { desc: '쓸 수 있는 도구 보기' },
50
52
  skills: { desc: '스킬 보기·검색·골라 올리기', arg: '[검색어|all|off]' },
51
53
  plugin: { desc: '플러그인 목록·설치·삭제·반입묶음', arg: '[install|remove|pack]' },
52
54
  cost: { desc: '이번 세션 사용량' },
53
55
  status: { desc: '연결 상태' },
54
56
  scan: { desc: '이 PC 의 로컬 모델 서버 훑기', arg: '[save]' },
57
+ thread: { desc: '대화 갈래 — 곁가지를 딴 자리에서. 연결·되돌리기는 같이 씀', arg: '[new|fork|close|번호|이름]' },
58
+ learned: { desc: '쓰면서 저절로 알게 된 것 — 되는 명령·이 모델의 버릇', arg: '[지우기]' },
55
59
  sessions:{ desc: '이 폴더의 지난 대화 목록' },
56
60
  recall: { desc: '지난 대화에서 찾기 — 목록 말고 내용으로', arg: '<찾을 말>' },
57
61
  mcp: { desc: '밖에서 붙인 도구(MCP) 서버 보기' },
@@ -258,6 +262,9 @@ export async function handle(line, session, ctx) {
258
262
 
259
263
  case 'diff': return 바뀐것보기(session, ctx, arg), { handled: true };
260
264
 
265
+ case 'preview':
266
+ case 'serve': return await 미리보기(session, ctx, arg), { handled: true };
267
+
261
268
  case 'tools': {
262
269
  rule('도구', 70);
263
270
  for (const [n, t] of Object.entries(TOOLS)) {
@@ -525,6 +532,12 @@ export async function handle(line, session, ctx) {
525
532
  return { handled: true };
526
533
  }
527
534
 
535
+ case 'thread':
536
+ case '갈래': return 갈래명령(session, ctx, arg), { handled: true };
537
+
538
+ case 'learned':
539
+ case '배움': return 배움명령(session, ctx, arg), { handled: true };
540
+
528
541
  case 'sessions': {
529
542
  const rows = listSessions(session.root, { limit: 12 });
530
543
  rule('이 폴더의 지난 대화', 70);
@@ -677,6 +690,99 @@ async function doPlugin(session, arg) {
677
690
  say('');
678
691
  }
679
692
 
693
+ /*
694
+ * 만든 웹을 그 자리에서 띄운다.
695
+ *
696
+ * 여기 있는 것은 화면과 말뿐이고, 서버는 preview/serve.js 가 한다.
697
+ * 한 번에 하나만 띄운다 — 여러 개를 띄워 놓으면 어느 주소가 무엇인지
698
+ * 아무도 못 외우고, 끌 때도 뭘 껐는지 모른다.
699
+ */
700
+ let 미리보기중 = null;
701
+
702
+ /** 켜져 있으면 끈다. 프로그램을 끝낼 때도 이걸 부른다. */
703
+ export async function 미리보기끄기() {
704
+ if (!미리보기중) return null;
705
+ const 끈것 = 미리보기중;
706
+ 미리보기중 = null;
707
+ try { await 끈것.서버.닫기(); } catch { /* 이미 닫혔다 */ }
708
+ return 끈것;
709
+ }
710
+
711
+ /** 지금 띄워 둔 것. 상태줄·검사에서 본다. */
712
+ async function 미리보기(session, ctx, arg) {
713
+ const 말 = String(arg ?? '').trim();
714
+
715
+ if (/^(off|끄기|중지|stop)$/i.test(말)) {
716
+ const 끈것 = await 미리보기끄기();
717
+ say('');
718
+ say(끈것 ? ` ${mark.ok} 미리보기를 껐습니다. ${c.gray(끈것.서버.url)}`
719
+ : ` ${c.gray('띄워 둔 것이 없습니다.')}`);
720
+ say('');
721
+ return;
722
+ }
723
+
724
+ // 이미 떠 있는데 또 치면, 주소를 다시 알려 주고 브라우저만 연다.
725
+ // 여기서 조용히 하나 더 띄우면 포트가 둘이 되고 어느 쪽이 진짜인지 모르게 된다.
726
+ if (미리보기중 && !말) {
727
+ say('');
728
+ say(` ${c.hgreen('▶')} 이미 띄워 뒀습니다 ${c.cyan(미리보기중.서버.url)} ${c.gray(미리보기중.보인이름)}`);
729
+ 브라우저로(미리보기중.서버.url);
730
+ say(` ${c.gray('끄려면')} ${c.cyan('/preview off')}`);
731
+ say('');
732
+ return;
733
+ }
734
+ // 다른 폴더를 주면 앞엣것은 끄고 새로 띄운다.
735
+ if (미리보기중) await 미리보기끄기();
736
+
737
+ let 뿌리;
738
+ try {
739
+ 뿌리 = ctx.scope.resolve(말 || '.');
740
+ } catch (err) {
741
+ say('');
742
+ say(` ${c.red('띄울 수 없습니다')} ${c.gray(err.message)}`);
743
+ say('');
744
+ return;
745
+ }
746
+
747
+ if (!existsSync(뿌리)) {
748
+ say('');
749
+ say(` ${c.red('그런 폴더가 없습니다')} ${c.gray(ctx.scope.show(뿌리))}`);
750
+ say('');
751
+ return;
752
+ }
753
+ // 파일 하나를 줬으면 그 파일이 든 폴더를 띄우고, 브라우저는 그 파일로 연다.
754
+ let 첫주소 = '';
755
+ if (!statSync(뿌리).isDirectory()) {
756
+ 첫주소 = encodeURIComponent(뿌리.split(/[\\/]/).pop());
757
+ 뿌리 = join(뿌리, '..');
758
+ 뿌리 = ctx.scope.resolve(뿌리);
759
+ }
760
+
761
+ let 서버;
762
+ try {
763
+ 서버 = await 띄우기({ 뿌리, scope: ctx.scope });
764
+ } catch (err) {
765
+ say('');
766
+ say(` ${c.red('못 띄웠습니다')} ${c.gray(err.message)}`);
767
+ say('');
768
+ return;
769
+ }
770
+ 미리보기중 = { 서버, 보인이름: ctx.scope.show(뿌리) };
771
+
772
+ const url = 서버.url + 첫주소;
773
+ say('');
774
+ say(` ${c.hgreen('▶')} ${c.bold('띄웠습니다')} ${c.cyan(url)}`);
775
+ say(` ${c.gray('보여 주는 것')} ${c.white(미리보기중.보인이름)}`);
776
+ say(서버.되살아나나
777
+ ? ` ${c.gray('파일을 고치면 화면이 저절로 새로 뜹니다.')}`
778
+ : ` ${c.yellow('⚠')} ${c.gray('이 자리에서는 파일 변화를 못 봅니다 — 브라우저를 손으로 새로 고치세요.')}`);
779
+ // 어디까지 열리는지 반드시 말한다. '서버를 띄웠다' 는 말은 사람마다 다르게 읽힌다.
780
+ say(` ${c.gray('이 컴퓨터에서만 열립니다(127.0.0.1). 다른 PC 에서는 안 보입니다.')}`);
781
+ say(` ${c.gray('끄려면')} ${c.cyan('/preview off')}${c.gray(' · deel 을 끝내면 같이 꺼집니다.')}`);
782
+ 브라우저로(url);
783
+ say('');
784
+ }
785
+
680
786
  function showSkills(session, arg) {
681
787
  const all = session.skills ?? [];
682
788
  if (!all.length) {
@@ -733,9 +839,9 @@ function showSkills(session, arg) {
733
839
  if (hits.length > 30) say(` ${c.gray(`… 그 밖에 ${hits.length - 30}개`)}`);
734
840
  say('');
735
841
 
736
- const bySource = { project: 0, user: 0, plugin: 0 };
842
+ const bySource = { project: 0, user: 0, plugin: 0, builtin: 0 };
737
843
  for (const s of all) bySource[s.source] = (bySource[s.source] ?? 0) + 1;
738
- say(` ${c.gray('전체')} ${all.length}개 ${c.gray('(프로젝트')} ${bySource.project} ${c.gray('· 사용자')} ${bySource.user} ${c.gray('· 플러그인')} ${bySource.plugin}${c.gray(')')}`);
844
+ say(` ${c.gray('전체')} ${all.length}개 ${c.gray('(프로젝트')} ${bySource.project} ${c.gray('· 사용자')} ${bySource.user} ${c.gray('· 플러그인')} ${bySource.plugin} ${c.gray('· 품고 다니는 것')} ${bySource.builtin}${c.gray(')')}`);
739
845
  say(` ${c.gray('프롬프트에 올라간 것')} ${session.listedSkills().length}개 ${c.gray(`(상한 ${session.maxSkillsListed})`)}`);
740
846
  if ((session.plugins ?? []).length) {
741
847
  say(` ${c.gray('플러그인')} ${session.plugins.filter((p) => p.skills > 0).map((p) => p.name).slice(0, 8).join(', ')}`);
@@ -884,6 +990,154 @@ function showThink(session, { 자세히 = false } = {}) {
884
990
  say('');
885
991
  }
886
992
 
993
+ /**
994
+ * 대화 갈래.
995
+ *
996
+ * 여기서 하는 일은 화면과 말뿐이다. 갈래를 들고 있는 것은 agent/threads.js 다.
997
+ */
998
+ function 갈래명령(session, ctx, arg = '') {
999
+ const 갈래 = ctx?.갈래;
1000
+ say('');
1001
+ if (!갈래) {
1002
+ // 한 번 돌리고 끝내는 자리(-p)에는 갈래가 없다. 없는 것을 있는 척하지 않는다.
1003
+ say(` ${c.gray('이 자리에서는 갈래를 못 씁니다 — 대화 화면에서만 됩니다.')}`);
1004
+ say('');
1005
+ return;
1006
+ }
1007
+
1008
+ const 말 = String(arg ?? '').trim();
1009
+ const [머리, ...나머지] = 말.split(/\s+/);
1010
+ const 뒷말 = 나머지.join(' ');
1011
+ const 알림 = (g) => {
1012
+ say(` ${c.hcyan('⑂')} ${c.bold(g.이름)} ${c.gray('갈래로 왔습니다.')} `
1013
+ + c.gray(session.messages.length ? `오간 말 ${session.messages.length}개` : '빈 대화입니다'));
1014
+ };
1015
+
1016
+ if (/^(new|새|새로)$/i.test(머리 ?? '')) {
1017
+ 알림(갈래.새로(뒷말));
1018
+ say(` ${c.gray('본줄기로 돌아가려면')} ${c.cyan('/thread 1')}`);
1019
+ say('');
1020
+ return;
1021
+ }
1022
+
1023
+ if (/^(fork|갈라|분기)$/i.test(머리 ?? '')) {
1024
+ const g = 갈래.갈라내기(뒷말);
1025
+ say(` ${c.hcyan('⑂')} ${c.bold(g.이름)} ${c.gray('로 갈라 나왔습니다.')} `
1026
+ + c.gray(`여기까지 오간 말 ${session.messages.length}개를 그대로 물려받았습니다.`));
1027
+ say(` ${c.gray('여기서 무엇을 하든 본줄기는 그대로입니다.')}`);
1028
+ say('');
1029
+ return;
1030
+ }
1031
+
1032
+ if (/^(close|닫기|끝)$/i.test(머리 ?? '')) {
1033
+ const r = 갈래.닫기(뒷말);
1034
+ if (!r.ok) say(` ${c.red('못 닫았습니다')} ${c.gray(r.why)}`);
1035
+ else {
1036
+ say(` ${mark.ok} ${c.gray(`${r.닫은것.이름} 갈래를 닫았습니다. 적어 둔 것은 남아 있습니다 —`)} ${c.cyan('/sessions')}`);
1037
+ 알림(r.지금);
1038
+ }
1039
+ say('');
1040
+ return;
1041
+ }
1042
+
1043
+ if (말) {
1044
+ const g = 갈래.옮기기(말);
1045
+ if (!g) {
1046
+ say(` ${c.red('그런 갈래가 없습니다')} ${c.gray(말)}`);
1047
+ say(` ${c.gray('/thread 만 치면 목록이 나옵니다.')}`);
1048
+ } else 알림(g);
1049
+ say('');
1050
+ return;
1051
+ }
1052
+
1053
+ // 그냥 /thread — 목록.
1054
+ rule('대화 갈래', 70);
1055
+ for (const r of 갈래.목록()) {
1056
+ const 표 = r.지금 ? c.hcyan('▶') : c.gray(' ');
1057
+ const 이름 = r.지금 ? c.white(r.이름) : c.gray(r.이름);
1058
+ say(` ${표} ${c.gray(String(r.번호))} ${pad(이름, 24)} ${c.gray(`말 ${String(r.말수).padStart(3)}개`)}`
1059
+ + (r.id ? ` ${c.gray(r.id)}` : ''));
1060
+ }
1061
+ say('');
1062
+ say(` ${c.gray('/thread new [이름]')} ${c.gray('빈 갈래로 나간다 — 곁가지 질문을 여기서')}`);
1063
+ say(` ${c.gray('/thread fork [이름]')} ${c.gray('지금까지를 물려받아 갈라 나간다')}`);
1064
+ say(` ${c.gray('/thread <번호>')} ${c.gray('그 갈래로 옮긴다')}`);
1065
+ say(` ${c.gray('/thread close')} ${c.gray('지금 갈래를 닫는다')}`);
1066
+ say('');
1067
+ say(` ${c.gray('연결·모델·도구·되돌리기는 갈래끼리 같이 씁니다. 오간 말과 토큰만 따로입니다.')}`);
1068
+ say('');
1069
+ }
1070
+
1071
+ /**
1072
+ * 쓰면서 저절로 알게 된 것.
1073
+ *
1074
+ * 보여 주는 것이 중요하다. 프롬프트에 몰래 들어가는 글이 있으면 사람은
1075
+ * 모델이 왜 그렇게 답했는지 알 수 없게 된다 — 여기서 통째로 볼 수 있어야
1076
+ * '자동으로 쌓인다' 가 무섭지 않은 말이 된다. 지우는 길도 같이 둔다.
1077
+ */
1078
+ function 배움명령(session, ctx, arg = '') {
1079
+ const 배움 = ctx?.배움;
1080
+ say('');
1081
+ if (!배움) {
1082
+ say(` ${c.gray('이 자리에서는 못 봅니다 — 대화 화면에서만 됩니다.')}`);
1083
+ say('');
1084
+ return;
1085
+ }
1086
+
1087
+ if (/^(지우기|clear|forget|비우기)$/i.test(String(arg).trim())) {
1088
+ 배움.지우기('전부');
1089
+ session.배움요약 = null;
1090
+ say(` ${mark.ok} ${c.gray('쌓아 둔 것을 비웠습니다. 다시 겪으면서 새로 쌓습니다.')}`);
1091
+ say('');
1092
+ return;
1093
+ }
1094
+
1095
+ const { 명령, 모델, 모델이름 } = 배움.현황(session.conn.model);
1096
+ rule('겪어 본 것', 70);
1097
+
1098
+ if (!명령.length && !모델) {
1099
+ say(` ${c.gray('아직 쌓인 것이 없습니다. 명령을 돌리고 대화를 나눌수록 여기가 찹니다.')}`);
1100
+ say('');
1101
+ return;
1102
+ }
1103
+
1104
+ if (명령.length) {
1105
+ say(` ${c.bold('이 폴더에서 돌려 본 명령')}`);
1106
+ for (const r of 명령.slice(0, 12)) {
1107
+ const 표 = r.no === 0 ? c.green('✓') : r.ok === 0 ? c.red('✗') : c.yellow('~');
1108
+ say(` ${표} ${pad(r.이름, 24)} ${c.gray(`됨 ${r.ok} · 안 됨 ${r.no}`)}`);
1109
+ }
1110
+ say('');
1111
+ }
1112
+
1113
+ if (모델) {
1114
+ say(` ${c.bold('이 모델에 대해')} ${c.gray(모델이름)}`);
1115
+ const 걸음 = 모델.걸음 ?? 0;
1116
+ const 줄 = (이름, n) => {
1117
+ if (!n) return;
1118
+ const 비율 = 걸음 ? Math.round((n / 걸음) * 100) : 0;
1119
+ say(` ${c.gray(pad(이름, 24))} ${pad(String(n), 5, 'right')} ${c.gray(걸음 ? `(${비율}%)` : '')}`);
1120
+ };
1121
+ say(` ${c.gray(pad('같이 걸어 본 걸음', 24))} ${pad(String(걸음), 5, 'right')}`);
1122
+ 줄('인자가 잘림', 모델.잘린인자);
1123
+ 줄('빈 답', 모델.빈답);
1124
+ 줄('편집이 빗나감', 모델.편집실패);
1125
+ if (모델.보정) say(` ${c.gray(pad('토큰 추정 보정', 24))} ${pad(`×${모델.보정.toFixed(2)}`, 5, 'right')}`);
1126
+ say('');
1127
+ }
1128
+
1129
+ const 실린것 = 배움.요약(session.conn.model);
1130
+ if (실린것) {
1131
+ say(` ${c.bold('이 중 프롬프트에 실리는 것')}`);
1132
+ for (const l of 실린것.split('\n').slice(1)) say(` ${c.gray(l)}`);
1133
+ } else {
1134
+ say(` ${c.gray('아직 프롬프트에 실을 만큼 확실한 것은 없습니다 — 두 번 이상 겪어야 싣습니다.')}`);
1135
+ }
1136
+ say('');
1137
+ say(` ${c.gray('지우려면')} ${c.cyan('/learned 지우기')}`);
1138
+ say('');
1139
+ }
1140
+
887
1141
  function showContext(session) {
888
1142
  const b = session.breakdown();
889
1143
  say('');
@@ -900,7 +1154,17 @@ function showContext(session) {
900
1154
  say(` ${c.gray(pad('남음', 26))} ${pad(b.left.toLocaleString(), 8, 'right')}`);
901
1155
  say('');
902
1156
  say(` ${c.gray('/compact 대화 줄이기 /clear 통째로 비우기')}`);
903
- say(` ${c.gray('숫자는 추정입니다 — 정확한 토크나이저를 쓰지 않습니다.')}`);
1157
+ /*
1158
+ * 추정이라고만 적어 두면 사람은 얼마나 믿어야 할지 모른다. 서버가 알려 준
1159
+ * 실제값에 맞춰 가고 있으면 그 사실을 적는다 — '추정' 과 '맞춰 본 추정' 은
1160
+ * 믿을 만한 정도가 다르다.
1161
+ */
1162
+ if (b.보정잰것 > 0) {
1163
+ const 차이 = Math.round((b.보정 - 1) * 100);
1164
+ say(` ${c.gray(`서버가 알려 준 실제값에 맞춰 ${차이 >= 0 ? '+' : ''}${차이}% 보정했습니다 (${b.보정잰것}번 재봄).`)}`);
1165
+ } else {
1166
+ say(` ${c.gray('숫자는 추정입니다 — 정확한 토크나이저를 쓰지 않습니다.')}`);
1167
+ }
904
1168
  say('');
905
1169
  }
906
1170
 
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // 연결 프로필 저장/읽기. ~/.deel/config.json (프로젝트 폴더의 .deel/config.json 이 우선)
2
2
  import { homedir } from 'node:os';
3
- import { join, dirname, resolve } from 'node:path';
3
+ import { join, resolve } from 'node:path';
4
4
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs';
5
5
 
6
6
  // 설정이 놓이는 자리.
@@ -19,6 +19,15 @@ function projectDir() {
19
19
  return join(process.cwd(), '.deel');
20
20
  }
21
21
 
22
+ /**
23
+ * 이 PC 의 설정 폴더 (`~/.deel` 또는 DEEL_HOME).
24
+ *
25
+ * configPath() 와 다르다 — 저쪽은 프로젝트에 설정이 있으면 그쪽을 준다.
26
+ * 여기는 언제나 **이 PC 것**이다. 모델에 대해 알아낸 것처럼 폴더를 옮겨도
27
+ * 따라와야 하는 것을 여기 둔다 (agent/evolve.js).
28
+ */
29
+ export function homeDir() { return userDir(); }
30
+
22
31
  export function configPath() {
23
32
  const local = join(projectDir(), 'config.json');
24
33
  if (existsSync(local)) return local;
package/src/oneshot.js CHANGED
@@ -318,6 +318,10 @@ export async function runOnce(opts = {}) {
318
318
  곁(` ${c.yellow('↻')} ${c.gray(`${ev.why} — 상한을 ${ev.from} → ${ev.to} 로 올려 다시 부릅니다`)}`);
319
319
  break;
320
320
 
321
+ case 'folded':
322
+ 곁(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 (${ev.아낀토큰.toLocaleString()} 토큰을 비움)`)}`);
323
+ break;
324
+
321
325
  case 'compacted':
322
326
  곁(` ${c.cyan('◱')} ${c.gray(`대화 ${ev.folded}개를 요약으로 접었습니다 (${ev.before.toLocaleString()} → ${ev.after.toLocaleString()} 토큰)`)}`);
323
327
  break;