deel-local-cli 1.7.0 → 1.10.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 (61) hide show
  1. package/README.ko.md +1166 -0
  2. package/README.md +1223 -1107
  3. package/bin/deel.js +78 -12
  4. package/package.json +4 -4
  5. package/src/acp/map.js +1 -1
  6. package/src/acp/serve.js +40 -3
  7. package/src/agent/compact.js +314 -296
  8. package/src/agent/effort.js +27 -7
  9. package/src/agent/evidence.js +2 -0
  10. package/src/agent/filemem.js +141 -0
  11. package/src/agent/grade.js +51 -1
  12. package/src/agent/loop.js +218 -40
  13. package/src/agent/session.js +136 -14
  14. package/src/agent/store.js +186 -9
  15. package/src/agent/threads.js +26 -1
  16. package/src/backend/adapter.js +272 -23
  17. package/src/backend/learn.js +24 -0
  18. package/src/backend/mcp.js +96 -5
  19. package/src/backend/probe.js +131 -61
  20. package/src/backend/quota.js +29 -5
  21. package/src/backend/retry.js +3 -0
  22. package/src/backend/scanui.js +1 -1
  23. package/src/backend/toolfit.js +325 -0
  24. package/src/commands.js +170 -61
  25. package/src/completion.js +20 -2
  26. package/src/config.js +47 -2
  27. package/src/i18n/en.js +210 -3
  28. package/src/i18n/index.js +40 -0
  29. package/src/i18n/ja.js +204 -3
  30. package/src/i18n/ko.js +270 -3
  31. package/src/i18n/zh.js +204 -3
  32. package/src/lsp/client.js +49 -5
  33. package/src/oneshot.js +92 -5
  34. package/src/pack/sbom.js +30 -4
  35. package/src/pack/selfpack.js +26 -10
  36. package/src/pack/sheet.en.js +288 -0
  37. package/src/pack/tar.js +65 -2
  38. package/src/plugins/manage.js +46 -9
  39. package/src/repl.js +344 -61
  40. package/src/reset.js +397 -0
  41. package/src/safety/audit.js +95 -8
  42. package/src/safety/authcmd.js +316 -0
  43. package/src/safety/guard.js +143 -0
  44. package/src/safety/keystore.js +114 -38
  45. package/src/safety/undo.js +25 -6
  46. package/src/tools/desc.en.js +19 -14
  47. package/src/tools/index.js +349 -75
  48. package/src/tools/jobs.js +175 -42
  49. package/src/tools/lsp.js +5 -4
  50. package/src/tools/outline.js +7 -3
  51. package/src/tools/task.js +10 -6
  52. package/src/tools/todo.js +9 -2
  53. package/src/tools/verify.js +42 -7
  54. package/src/tools/webfetch.js +94 -10
  55. package/src/ui/export.js +1 -1
  56. package/src/ui/md.js +201 -5
  57. package/src/ui/motion.js +0 -1
  58. package/src/ui/pastechip.js +50 -3
  59. package/src/ui/pick.js +115 -0
  60. package/src/ui/screen.js +23 -3
  61. package/README.en.md +0 -1117
package/src/commands.js CHANGED
@@ -12,7 +12,8 @@ import { allowEndpoint } from './safety/network.js';
12
12
  import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
13
13
  import { 주소가리기 } from './safety/secrets.js';
14
14
  import { pick, confirm } from './ui/prompt.js';
15
- import { load, save, resolveKey, upsert, 열쇠보관, configPath } from './config.js';
15
+ import { load, save, 저장시도, resolveKey, upsert, 열쇠보관, configPath } from './config.js';
16
+ import { 지금상태 as 지금열쇠상태 } from './safety/authcmd.js';
16
17
  import { 제공자고르기 } from './providers/index.js';
17
18
  import { 종, 알릴만한초 } from './ui/notify.js';
18
19
  import { 말, 언어, 언어들, 언어정하기, 언어고르기, 옮긴만큼, 지시말, 지시말정하기, 지시말따로정했나 } from './i18n/index.js';
@@ -22,7 +23,7 @@ import { chat, 규격이름 } from './backend/adapter.js';
22
23
  import { 알림채움 } from './backend/retry.js';
23
24
  import { 프록시고르기, 프록시설정 } from './backend/proxy.js';
24
25
  import { 정한셸 } from './tools/shell.js';
25
- import { TOOLS } from './tools/index.js';
26
+ import { TOOLS, 영어설명 } from './tools/index.js';
26
27
  import { 둘러보기, 프로젝트갈래 } from './lsp/servers.js';
27
28
  import { 지금것들 } from './lsp/client.js';
28
29
  import { 보고서적기 } from './ui/export.js';
@@ -170,6 +171,23 @@ function 경로처럼보이나(line) {
170
171
  }
171
172
 
172
173
  // 반환: { handled, exit? } handled=false 면 모델에게 보낸다.
174
+
175
+ /**
176
+ * 설정을 남기고, 못 남겼으면 화면에 한 줄 (config.js 의 저장시도).
177
+ *
178
+ * 여태 아홉 자리가 전부 `try { save(cfg) } catch {}` 였다. 못 남겨도 이번 판에는
179
+ * 먹으니 대화는 계속되는데, 바로 다음 줄에서 화면은 `✓ 바꿨습니다` 를 찍는다.
180
+ * 사람은 정해진 줄 알고 창을 닫았다가 다음에 옛 값을 보고, 그때는 무엇 때문인지
181
+ * 알 길이 없다 — 홈이 읽기 전용인지, 디스크가 찼는지.
182
+ *
183
+ * @returns {boolean} 남겼나
184
+ */
185
+ function 설정남기기(cfg, 옵션 = {}) {
186
+ const r = 저장시도(cfg, 옵션);
187
+ if (!r.ok) say(` ${mark.warn} ${c.yellow(말('common.cfgSaveFailed', { 왜: clip(r.왜, 70) }))}`);
188
+ return r.ok;
189
+ }
190
+
173
191
  /** /mode 아래에 규칙을 늘어놓는다. 아무것도 안 걸려 있으면 아무 말도 안 한다. */
174
192
  function 규칙보이기(규칙들) {
175
193
  if (!규칙들) return;
@@ -410,7 +428,7 @@ export async function handle(line, session, ctx) {
410
428
  // 「화면 말을 따라간다」 가 아니라 옛 값이 되살아난다.
411
429
  if (지시말따로정했나()) cfg.promptLang = 지시말();
412
430
  else delete cfg.promptLang;
413
- try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
431
+ 설정남기기(cfg);
414
432
  const p = 옮긴만큼();
415
433
  say('');
416
434
  say(` ${mark.ok} ${말('lang.changed', { 이름: 이름(언어()) })}`);
@@ -541,7 +559,7 @@ export async function handle(line, session, ctx) {
541
559
  return { handled: true };
542
560
  }
543
561
  cfg.bell = 켜는말.includes(값);
544
- try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
562
+ 설정남기기(cfg);
545
563
  say('');
546
564
  say(` ${mark.ok} ${말(cfg.bell ? 'bell.turnedOn' : 'bell.turnedOff')}`);
547
565
  // 켤 때는 한 번 울려 준다. "켰다는데 소리가 나나?" 를 그 자리에서 확인하게.
@@ -605,7 +623,7 @@ export async function handle(line, session, ctx) {
605
623
  }
606
624
 
607
625
  cfg.motion = 고른것.값;
608
- try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
626
+ 설정남기기(cfg);
609
627
  적용하기(cfg.motion);
610
628
 
611
629
  say('');
@@ -821,18 +839,32 @@ export async function handle(line, session, ctx) {
821
839
  case 'undo': {
822
840
  const n = Math.max(1, parseInt(arg, 10) || 1);
823
841
  const r = ctx.history.undo(n);
824
- ctx.audit.undo({ turns: r.turns, files: r.restored.length });
842
+ /*
843
+ * 세는 수는 **진짜로 되돌아간 것**이다.
844
+ *
845
+ * 전에는 restored.length 를 그대로 썼다. 그 배열에는 못 되돌린 것과
846
+ * 손대지 않은 것(바이너리처럼 내용을 못 떠 둔 파일)까지 들어 있어서,
847
+ * 하나도 못 되돌린 판에도 「파일 2개를 되돌렸습니다」 가 나올 수 있었다.
848
+ * 감사기록에도 그 부풀린 수가 그대로 남았다.
849
+ */
850
+ const 되돌린수 = r.되돌린수 ?? r.restored.length;
851
+ ctx.audit.undo({ turns: r.turns, files: 되돌린수 });
825
852
  if (!r.restored.length) {
826
853
  say(` ${c.gray(말('undo.nothing'))}`);
827
854
  say('');
828
855
  return { handled: true };
829
856
  }
830
- say(` ${mark.ok} ${말('undo.done', { turns: r.turns, files: r.restored.length })}`);
857
+ say(` ${mark.ok} ${말('undo.done', { turns: r.turns, files: 되돌린수 })}`);
831
858
  for (const f of r.restored) say(` ${c.gray(ctx.scope.show(f.path))} ${c.gray(f.how)}`);
859
+ // 못 되돌린 것이 있으면 그것만 따로 못 박는다. 위 목록에 섞여 있으면 안 읽힌다.
860
+ if (r.못한것?.length) {
861
+ say(` ${mark.warn} ${말('undo.failed', { n: r.못한것.length })}`);
862
+ }
832
863
 
833
864
  // /diff 가 세던 것에서도 뺀다. 되돌린 파일이 '바뀐 파일' 로 남아 있으면
834
- // 사람도 되돌아간 줄 모른다.
835
- for (const f of r.restored) if (!f.skipped) session.changes.delete(f.path);
865
+ // 사람도 되돌아간 줄 모른다. **되돌아간 것만** 뺀다 — 못 되돌린 파일을
866
+ // 여기서 빼면 진짜로 바뀌어 있는 파일이 /diff 에서도 사라진다.
867
+ for (const f of r.restored) if (f.ok === true) session.changes.delete(f.path);
836
868
  // 상태줄의 '되돌릴 턴' 도 여기서 줄여 준다. 안 줄이면 방금 되돌린 것을
837
869
  // 아직 되돌릴 수 있는 것처럼 세고 있게 된다.
838
870
  try { session.되돌릴턴 = ctx.history.turns().length; } catch { /* 못 세면 그냥 둔다 */ }
@@ -863,9 +895,20 @@ export async function handle(line, session, ctx) {
863
895
  case 'serve': return await 미리보기(session, ctx, arg), { handled: true };
864
896
 
865
897
  case 'tools': {
866
- rule('도구', 70);
898
+ rule(말('scr.tools'), 70);
899
+ /*
900
+ * 원본 schema.description 을 그대로 찍으면 안 된다.
901
+ *
902
+ * 그 글은 한국어다. 영어 표(tools/desc.en.js)는 **모델에게 줄 때만**
903
+ * 갈아 끼우고 있어서, 영어로 켠 사람의 `/tools` 는 스물한 줄이 통째로
904
+ * 한국어로 나왔다. `/lang` 은 그 사이에도 100% 라고 답했다 — 이 글들은
905
+ * 애초에 말 표에 들어간 적이 없어서 셈에 안 잡혔기 때문이다.
906
+ *
907
+ * 여기서는 **화면 말**(언어())로 고른다. 모델에게 주는 글의 말과는
908
+ * 다른 축이다 — 영어로 시키고 한국어로 보는 조합이 실제로 쓰인다.
909
+ */
867
910
  for (const [n, t] of Object.entries(TOOLS)) {
868
- say(` ${c.cyan(pad(n, 8))} ${c.gray(t.schema.description)}`);
911
+ say(` ${c.cyan(pad(n, 8))} ${c.gray(영어설명(t.schema, n, 언어()).description)}`);
869
912
  }
870
913
  say('');
871
914
  return { handled: true };
@@ -873,14 +916,14 @@ export async function handle(line, session, ctx) {
873
916
 
874
917
  case 'cost': {
875
918
  const mins = ((Date.now() - session.startedAt) / 60000).toFixed(1);
876
- rule('이번 세션', 70);
877
- say(` ${c.gray(pad('모델 호출', 14))} ${session.usage.calls}회`);
878
- say(` ${c.gray(pad('입력 토큰', 14))} ${session.usage.in.toLocaleString()}`);
879
- say(` ${c.gray(pad('출력 토큰', 14))} ${session.usage.out.toLocaleString()}`);
880
- say(` ${c.gray(pad('도구 시간', 14))} ${(session.usage.ms / 1000).toFixed(1)}초`);
919
+ rule(말('scr.session'), 70);
920
+ say(` ${c.gray(pad(말('cost.calls'), 14))} ${말('unit.calls', { n: session.usage.calls })}`);
921
+ say(` ${c.gray(pad(말('cost.tokIn'), 14))} ${session.usage.in.toLocaleString()}`);
922
+ say(` ${c.gray(pad(말('cost.tokOut'), 14))} ${session.usage.out.toLocaleString()}`);
923
+ say(` ${c.gray(pad(말('cost.toolMs'), 14))} ${('unit.sec', { n: (session.usage.ms / 1000).toFixed(1) })}`);
881
924
  // 서버가 잠깐 막아 다시 부른 횟수. 0 이면 안 적는다 — 없는 일을 줄로 남기면 표만 길어진다.
882
- if (session.usage.retries) say(` ${c.gray(pad(말('cost.retries'), 14))} ${session.usage.retries}회`);
883
- say(` ${c.gray(pad('경과', 14))} ${mins}분`);
925
+ if (session.usage.retries) say(` ${c.gray(pad(말('cost.retries'), 14))} ${말('unit.calls', { n: session.usage.retries })}`);
926
+ say(` ${c.gray(pad(말('cost.elapsed'), 14))} ${말('unit.min', { n: mins })}`);
884
927
  /*
885
928
  * 돈.
886
929
  *
@@ -897,7 +940,7 @@ export async function handle(line, session, ctx) {
897
940
  const 요금값 = 세션요금(session);
898
941
  const 쓴돈 = 돈셈(session.usage, 요금값);
899
942
  if (쓴돈) {
900
- say(` ${c.gray(pad('요금', 14))} ${c.bold(돈말(쓴돈.달러))}`
943
+ say(` ${c.gray(pad(말('cost.money'), 14))} ${c.bold(돈말(쓴돈.달러))}`
901
944
  + ` ${c.gray(`(${어디서온값(요금값)})`)}`);
902
945
  } else if (바깥인가(session.conn.base) && (session.usage.in || session.usage.out)) {
903
946
  const 붙은곳 = session.제공자 ? 제공자고르기(session.제공자) : null;
@@ -917,8 +960,8 @@ export async function handle(line, session, ctx) {
917
960
  const 남은것 = 마지막할당량();
918
961
  if (남은것) {
919
962
  const 말줄 = 할당량말(남은것);
920
- say(` ${c.gray(pad('서버 할당량', 14))} ${아슬아슬한가(남은것) ? c.yellow(말줄) : 말줄}`
921
- + ` ${c.gray(`(${Math.round((Date.now() - 남은것.때) / 1000)}초 전 응답 기준)`)}`);
963
+ say(` ${c.gray(pad(말('cost.quota'), 14))} ${아슬아슬한가(남은것) ? c.yellow(말줄) : 말줄}`
964
+ + ` ${c.gray(`(${말('cost.quotaAge', { 초: Math.round((Date.now() - 남은것.때) / 1000) })})`)}`);
922
965
  }
923
966
  say('');
924
967
  return { handled: true };
@@ -926,29 +969,53 @@ export async function handle(line, session, ctx) {
926
969
 
927
970
  case 'status': {
928
971
  const k = session.conn;
929
- rule('연결', 70);
930
- say(` ${c.gray(pad('규격', 10))} ${규격이름(k.kind)}`);
931
- say(` ${c.gray(pad('주소', 10))} ${k.base}`);
972
+ rule(말('scr.conn'), 70);
973
+ say(` ${c.gray(pad(말('status.kind'), 10))} ${규격이름(k.kind)}`);
974
+ say(` ${c.gray(pad(말('status.url'), 10))} ${k.base}`);
932
975
  // 프록시를 거치면 어느 것을, 어디서 읽었는지(env · config)까지. 안 거치면 줄 자체가 없다.
933
976
  // 적어 놨는데 못 쓰는 것(socks5 등)이면 그 까닭을 — 조용히 직접 가면 사람은 프록시를 탄 줄 안다.
934
977
  const 프록시 = 프록시고르기(k.base);
935
- if (프록시) say(` ${c.gray(pad('프록시', 10))} ${프록시.url} ${c.gray(`(${프록시.출처})`)}`);
936
- else if (프록시설정().탈) say(` ${c.gray(pad('프록시', 10))} ${c.yellow('못 씀')} ${c.gray(`— ${프록시설정().탈}`)}`);
937
- say(` ${c.gray(pad('모델', 10))} ${k.model}`);
938
- say(` ${c.gray(pad('작업 폴더', 10))} ${session.root}`);
978
+ if (프록시) say(` ${c.gray(pad(말('status.proxy'), 10))} ${프록시.url} ${c.gray(`(${프록시.출처})`)}`);
979
+ else if (프록시설정().탈) say(` ${c.gray(pad(말('status.proxy'), 10))} ${c.yellow(말('status.proxyOff'))} ${c.gray(`— ${프록시설정().탈}`)}`);
980
+ say(` ${c.gray(pad(말('status.model'), 10))} ${k.model}`);
981
+ say(` ${c.gray(pad(말('status.root'), 10))} ${session.root}`);
939
982
  // 명령이 어느 셸에서 도는지. "ls 가 왜 안 되지" 의 답이 이 줄에 있다.
940
- say(` ${c.gray(pad('', 10))} ${정한셸().표시}`);
983
+ say(` ${c.gray(pad(말('status.shell'), 10))} ${정한셸().표시}`);
941
984
  // 열쇠를 어디에 두고 있나. 사내 심사에서 제일 먼저 묻는 것이라
942
985
  // 「어딘가 잠겨 있겠지」 로 두지 않고 지금 상태를 그대로 적는다.
943
- if (k.auth !== 'none') say(` ${c.gray(pad('열쇠 보관', 10))} ${열쇠보관(load())}`);
944
- say(` ${c.gray(pad('규칙', 10))} ${session.rules ? session.rules.name : '없음 (/init 으로 만들 수 있습니다)'}`);
986
+ /*
987
+ * 열쇠를 어디에 두고 있나 또는 **어디서 받아 오나.**
988
+ *
989
+ * 받아 오는 연결에서는 「보관」 을 적으면 거짓말이 된다. 우리는 그
990
+ * 열쇠를 갖고 있지 않다. 사내 심사에서 제일 먼저 묻는 자리라
991
+ * 있는 그대로 적는다 (safety/authcmd.js).
992
+ */
993
+ if (session.conn.열쇠받기) {
994
+ const 상태 = 지금열쇠상태();
995
+ const 남은말 = 상태
996
+ ? 말('auth.leftMin', { 분: Math.round(상태.남은초 / 60) })
997
+ : 말('auth.notYet');
998
+ say(` ${c.gray(pad(말('status.keyStore'), 10))} `
999
+ + `${말(session.conn.열쇠받기.곳 === '정책' ? 'auth.fromPolicy' : 'auth.from')} ${c.gray(`· ${남은말}`)}`);
1000
+ say(` ${c.gray(pad('', 10))} ${c.gray(clip(session.conn.열쇠받기.명령, 56))}`);
1001
+ } else if (k.auth !== 'none') {
1002
+ say(` ${c.gray(pad(말('status.keyStore'), 10))} ${열쇠보관(load())}`);
1003
+ }
1004
+ say(` ${c.gray(pad(말('status.rules'), 10))} ${session.rules ? session.rules.name : 말('status.noRules')}`);
1005
+ // 파일이 있는데 못 읽은 것을 '없음' 으로 적으면, 규칙을 적어 둔 사람은
1006
+ // 걸려 있다고 믿는다. 안 걸린 채로 도는 것이 여기서 제일 나쁜 모양이다.
1007
+ if (session.규칙못읽음) {
1008
+ say(` ${c.gray(pad('', 10))} ${mark.warn} ${c.yellow(말('status.rulesUnread', {
1009
+ 이름: session.규칙못읽음.이름, 까닭: session.규칙못읽음.까닭,
1010
+ }))}`);
1011
+ }
945
1012
  const caps = [
946
- k.tools ? c.green('도구') : c.red('도구'),
947
- k.streaming ? c.green('스트림') : c.gray('스트림'),
948
- k.json ? c.green('스키마') : c.gray('스키마'),
949
- k.think ? c.green('추론') : c.gray('추론'),
1013
+ k.tools ? c.green(말('status.capTools')) : c.red(말('status.capTools')),
1014
+ k.streaming ? c.green(말('status.capStream')) : c.gray(말('status.capStream')),
1015
+ k.json ? c.green(말('status.capSchema')) : c.gray(말('status.capSchema')),
1016
+ k.think ? c.green(말('status.capThink')) : c.gray(말('status.capThink')),
950
1017
  ].join(c.gray(' · '));
951
- say(` ${c.gray(pad('지원', 10))} ${caps}`);
1018
+ say(` ${c.gray(pad(말('status.caps'), 10))} ${caps}`);
952
1019
  say('');
953
1020
  return { handled: true };
954
1021
  }
@@ -1176,6 +1243,10 @@ export async function handle(line, session, ctx) {
1176
1243
  say(` ${c.gray('모델에게는')} ${c.white('mcp__<서버>__<도구>')} ${c.gray('라는 이름으로 보입니다.')}`);
1177
1244
  say(` ${c.yellow('※')} ${c.gray('이 도구들은 남의 프로그램이 돌립니다 —')} ${c.white('작업 범위(' + session.root + ') 를 안 지킵니다.')}`);
1178
1245
  say(` ${c.gray(' 무엇을 불렀는지는')} ${c.white('.deel/audit.jsonl')} ${c.gray('에 남습니다.')}`);
1246
+ // 바로 위 줄이 약속이다. 못 지키고 있으면 여기서 말해야 한다.
1247
+ if (ctx?.audit?.못쓴것?.()) {
1248
+ say(` ${mark.warn} ${c.yellow('지금 그 파일에 못 적고 있습니다')} ${c.gray('— /증거 에서 까닭을 보세요.')}`);
1249
+ }
1179
1250
  say('');
1180
1251
  return { handled: true };
1181
1252
  }
@@ -1225,7 +1296,19 @@ export async function handle(line, session, ctx) {
1225
1296
  }
1226
1297
  say('');
1227
1298
  say(` ${c.gray('이어하려면 나갔다가')} ${c.cyan('deel --continue')} ${c.gray('또는')} ${c.cyan(`deel --resume ${rows[0].id}`)}`);
1228
- say(` ${c.gray('지금 대화는 나가지 않아도 계속 저장되고 있습니다.')}`);
1299
+ /*
1300
+ * 바로 아래 줄이 약속이다. 못 지키고 있으면 약속을 되풀이하면 안 된다.
1301
+ *
1302
+ * /mcp 화면에서 감사기록을 두고 한 것과 같은 자리다 — 「남습니다」 라고
1303
+ * 적어 놓고 실제로는 안 남는 것이 제일 나쁘다. 이 화면은 이어하기를
1304
+ * 보러 온 화면이라, 여기서 안 말하면 사람이 알 자리가 없다.
1305
+ */
1306
+ const 저장샘 = ctx?.갈래?.현재store?.()?.못쓴것?.();
1307
+ if (저장샘) {
1308
+ say(` ${mark.warn} ${c.yellow(말('store.notSaving', { n: 저장샘.수, 까닭: 저장샘.까닭 }))}`);
1309
+ } else {
1310
+ say(` ${c.gray('지금 대화는 나가지 않아도 계속 저장되고 있습니다.')}`);
1311
+ }
1229
1312
  say('');
1230
1313
  return { handled: true };
1231
1314
  }
@@ -1630,13 +1713,26 @@ function showThink(session, { 자세히 = false } = {}) {
1630
1713
  const 개발자 = session.level === '개발자';
1631
1714
  const 단계 = t.rows.map((r) => `${r.label} ${r.level}`).join(c.gray(' · '));
1632
1715
 
1716
+ /*
1717
+ * 이 화면은 여태 통째로 한국어였다.
1718
+ *
1719
+ * `/lang en` 으로 켠 사람에게 「추론 강도 medium (첫 판단 medium · …)」 이
1720
+ * 뜨면 아무것도 안 읽힌다. 명령 이름만 영어고 화면이 한국어인 자리는
1721
+ * 「덜 옮겨졌다」 가 아니라 「고장 났다」 로 읽힌다 — i18n/index.js 머리말.
1722
+ *
1723
+ * 예로 드는 명령도 말을 따라간다. `/think 배분 절약` 은 영어로 켠 사람이
1724
+ * 칠 수 없는 글자다. 두 이름 다 진짜로 받는다(위 배분말·자세히 갈래).
1725
+ */
1726
+ const 한국어 = 언어() === 'ko';
1727
+ const 배분예 = 한국어 ? '/think 배분 절약' : '/think profile save';
1728
+ const 자세히예 = 한국어 ? '/think 자세히' : '/think detail';
1633
1729
  say('');
1634
- say(` ${c.gray('추론 강도')} ${c.bold(session.think)} ${c.gray(`(${단계})`)}`);
1635
- if (개발자 || 자세히) say(` ${c.gray('배분')} ${c.bold(t.name)} ${c.gray(t.desc)}`);
1730
+ say(` ${c.gray(말('think.effort'))} ${c.bold(session.think)} ${c.gray(`(${단계})`)}`);
1731
+ if (개발자 || 자세히) say(` ${c.gray(pad(말('think.profile'), 한국어 ? 8 : 10))}${c.bold(t.name)} ${c.gray(t.desc)}`);
1636
1732
 
1637
1733
  if (자세히) {
1638
1734
  say('');
1639
- say(` ${c.gray(pad('단계', 12) + pad('강도', 10) + pad('출력상한', 10) + '언제')}`);
1735
+ say(` ${c.gray(pad(말('think.col.stage'), 12) + pad(말('think.col.level'), 10) + pad(말('think.col.cap'), 10) + 말('think.col.when'))}`);
1640
1736
  for (const r of t.rows) {
1641
1737
  const 화살 = r.moved > 0 ? c.yellow('↑') : r.moved < 0 ? c.cyan('↓') : c.gray('·');
1642
1738
  say(` ${pad(r.label, 12)}${화살} ${pad(r.level, 8)}${pad(r.cap.toLocaleString(), 10, 'right')} ${c.gray(r.why)}`);
@@ -1644,21 +1740,21 @@ function showThink(session, { 자세히 = false } = {}) {
1644
1740
  say('');
1645
1741
  // 상한이 어디서 왔는지 밝힌다. 세 줄이 같은 값일 때 그게 고장인지 아닌지
1646
1742
  // 이 한 줄로 갈린다 — 아는 상한이 낮으면 셋이 같아지는 것이 맞다.
1647
- say(` ${c.gray('출력 상한은')} ${c.white((아는상한 ?? 16384).toLocaleString())} ${c.gray(
1648
- session.conn.maxTokens ? '(직접 정하신 값)' : session.conn.maxOut ? '(서버에서 알아낸 값)' : '(모르는 값이라 기본값)',
1649
- )}${c.gray(' 안에서 나눕니다 ')} ${c.cyan('/out')}`);
1650
- say(` ${c.gray('컨텍스트')} ${c.white(t.ctx.toLocaleString())}${c.gray(' · 지금 찬 양 ')}${c.white(t.used.toLocaleString())}`);
1743
+ const 어디서 = session.conn.maxTokens ? 말('think.cap.you')
1744
+ : session.conn.maxOut ? ('think.cap.server') : 말('think.cap.guess');
1745
+ say(` ${c.gray(말('think.cap.head'))} ${c.white((아는상한 ?? 16384).toLocaleString())} ${c.gray(어디서)}${c.gray(말('think.cap.tail'))} ${c.cyan('/out')}`);
1746
+ say(` ${c.gray(말('think.ctx'))} ${c.white(t.ctx.toLocaleString())}${c.gray(말('think.ctx.used'))}${c.white(t.used.toLocaleString())}`);
1651
1747
  say('');
1652
- say(` ${c.gray('강도')} ${THINK_LEVELS.join(' · ')} ${c.gray('')} ${c.cyan('/think high')}`);
1653
- say(` ${c.gray('배분')} ${Object.entries(PROFILES).map(([k, v]) => `${k}(${v.name})`).join(' · ')} ${c.gray('')} ${c.cyan('/think 배분 절약')}`);
1748
+ say(` ${c.gray(말('think.effort'))} ${THINK_LEVELS.join(' · ')} ${c.gray(말('think.eg'))} ${c.cyan('/think high')}`);
1749
+ say(` ${c.gray(말('think.profile'))} ${Object.entries(PROFILES).map(([k, v]) => { const n = 한국어 ? v.name : (v.en ?? v.name); return n === k ? k : `${k}(${n})`; }).join(' · ')} ${c.gray(말('think.eg'))} ${c.cyan(배분예)}`);
1654
1750
  } else if (개발자) {
1655
- say(` ${c.gray('강도')} ${c.cyan('/think high')} ${c.gray('배분')} ${c.cyan('/think 배분 절약')} ${c.gray('자세히')} ${c.cyan('/think 자세히')}`);
1751
+ say(` ${c.gray(말('think.effort'))} ${c.cyan('/think high')} ${c.gray(말('think.profile'))} ${c.cyan(배분예)} ${c.gray(말('think.detail'))} ${c.cyan(자세히예)}`);
1656
1752
  } else {
1657
- say(` ${c.gray('더 세게')} ${c.cyan('/think high')} ${c.gray('더 빠르게')} ${c.cyan('/think low')}`);
1753
+ say(` ${c.gray(말('think.harder'))} ${c.cyan('/think high')} ${c.gray(말('think.faster'))} ${c.cyan('/think low')}`);
1658
1754
  }
1659
1755
 
1660
1756
  if (!session.conn.think && session.think !== 'off') {
1661
- say(` ${c.yellow('이 연결은 모델 층 강도 조절이 적용되지 않습니다.')} ${c.gray('출력 상한만 단계별로 적용됩니다.')}`);
1757
+ say(` ${c.yellow(말('think.nomodel'))} ${c.gray(말('think.nomodel.note'))}`);
1662
1758
  }
1663
1759
  say('');
1664
1760
  }
@@ -1760,6 +1856,19 @@ function 증거명령(session, ctx, arg = '') {
1760
1856
  say('');
1761
1857
  rule('작업 증거', 70);
1762
1858
 
1859
+ /*
1860
+ * 기록이 새고 있으면 **목록보다 먼저** 말한다.
1861
+ *
1862
+ * 이 화면은 감사기록을 읽어서 만든다. 기록이 안 적히면 목록이 짧아지는
1863
+ * 게 아니라 「아무것도 안 했다」로 보인다 — 아래 이른 반환이 그 자리다.
1864
+ * 안 한 것을 말하라고 만든 화면이 안 한 것처럼 보이게 하면 안 된다.
1865
+ */
1866
+ if (e.기록못씀) {
1867
+ say(` ${mark.warn} ${c.yellow(`감사기록 ${e.기록못씀.수}건이 안 적혔습니다`)} ${c.gray(`— ${clip(e.기록못씀.까닭, 60)}`)}`);
1868
+ say(` ${c.gray('아래는 실제로 한 것보다 짧습니다.')}`);
1869
+ say('');
1870
+ }
1871
+
1763
1872
  if (!e.바꾼것.length && !e.돌린것.length) {
1764
1873
  say(` ${c.gray('이번 대화에서 아직 바꾸거나 돌린 것이 없습니다.')}`);
1765
1874
  say('');
@@ -2219,8 +2328,8 @@ function 배움명령(session, ctx, arg = '') {
2219
2328
  function showContext(session) {
2220
2329
  const b = session.breakdown();
2221
2330
  say('');
2222
- rule('컨텍스트', 70);
2223
- say(` ${c.bold(session.conn.model)} ${c.gray('·')} ${b.total.toLocaleString()} 토큰`);
2331
+ rule(말('scr.context'), 70);
2332
+ say(` ${c.bold(session.conn.model)} ${c.gray('·')} ${말('unit.tokens', { n: b.total.toLocaleString() })}`);
2224
2333
  say('');
2225
2334
  say(` ${bar(b.used, b.total, 32)} ${b.used.toLocaleString()} / ${b.total.toLocaleString()} ${c.gray(`${Math.round((b.used / b.total) * 100)}%`)}`);
2226
2335
  say('');
@@ -2229,9 +2338,9 @@ function showContext(session) {
2229
2338
  say(` ${c.gray(pad(r.label, 26))} ${pad(r.n.toLocaleString(), 8, 'right')}`);
2230
2339
  }
2231
2340
  say(` ${c.gray('─'.repeat(35))}`);
2232
- say(` ${c.gray(pad('남음', 26))} ${pad(b.left.toLocaleString(), 8, 'right')}`);
2341
+ say(` ${c.gray(pad(말('ctx.left'), 26))} ${pad(b.left.toLocaleString(), 8, 'right')}`);
2233
2342
  say('');
2234
- say(` ${c.gray('/compact 대화 줄이기 /clear 통째로 비우기')}`);
2343
+ say(` ${c.gray(`/compact ${말('ctx.compactHint')} /clear ${말('ctx.clearHint')}`)}`);
2235
2344
  /*
2236
2345
  * 추정이라고만 적어 두면 사람은 얼마나 믿어야 할지 모른다. 서버가 알려 준
2237
2346
  * 실제값에 맞춰 가고 있으면 그 사실을 적는다 — '추정' 과 '맞춰 본 추정' 은
@@ -2239,9 +2348,9 @@ function showContext(session) {
2239
2348
  */
2240
2349
  if (b.보정잰것 > 0) {
2241
2350
  const 차이 = Math.round((b.보정 - 1) * 100);
2242
- say(` ${c.gray(`서버가 알려 실제값에 맞춰 ${차이 >= 0 ? '+' : ''}${차이}% 보정했습니다 (${b.보정잰것}번 재봄).`)}`);
2351
+ say(` ${c.gray(말('ctx.calibrated', { 부호: 차이 >= 0 ? '+' : '', 퍼센트: 차이, 번: b.보정잰것 }))}`);
2243
2352
  } else {
2244
- say(` ${c.gray('숫자는 추정입니다 — 정확한 토크나이저를 쓰지 않습니다.')}`);
2353
+ say(` ${c.gray(말('ctx.estimate'))}`);
2245
2354
  }
2246
2355
  say('');
2247
2356
  }
@@ -2348,7 +2457,7 @@ async function 출력상한(session, arg = '') {
2348
2457
 
2349
2458
  if (말 === 'auto' || 말 === '자동') {
2350
2459
  session.conn.maxTokens = null;
2351
- if (prof) { delete prof.maxTokens; try { save(cfg); } catch {} }
2460
+ if (prof) { delete prof.maxTokens; 설정남기기(cfg); }
2352
2461
  say(` ${mark.ok} 직접 정한 값을 지웠습니다.`);
2353
2462
  say(` ${c.gray(알아낸것 ? `서버에서 알아낸 ${알아낸것.toLocaleString()} 토큰을 씁니다.` : '모르는 값이라 16,384 토큰으로 갑니다.')}`);
2354
2463
  say('');
@@ -2364,7 +2473,7 @@ async function 출력상한(session, arg = '') {
2364
2473
  return;
2365
2474
  }
2366
2475
  session.conn.maxTokens = 값;
2367
- if (prof) { prof.maxTokens = 값; try { save(cfg); } catch {} }
2476
+ if (prof) { prof.maxTokens = 값; 설정남기기(cfg); }
2368
2477
  say(` ${mark.ok} 답 길이 상한 ${c.bold(값.toLocaleString())} 토큰`);
2369
2478
  say(` ${c.gray('모델이 못 내는 값을 넣으면 서버가 거절합니다. 거절당하면')} ${c.cyan('/out auto')} ${c.gray('로 되돌리세요.')}`);
2370
2479
  say('');
@@ -2736,7 +2845,7 @@ async function 나가도되나묻기(session, p) {
2736
2845
  async function 골라적용(session, cfg, p) {
2737
2846
  if (!await 나가도되나묻기(session, p)) return;
2738
2847
  cfg.active = p.id;
2739
- try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 바뀐다 */ }
2848
+ 설정남기기(cfg);
2740
2849
  연결적용(session, p);
2741
2850
  say(` ${mark.ok} ${c.bold(p.name)} ${c.gray(p.model)} 로 바꿨습니다. 대화는 이어집니다.`);
2742
2851
  say('');
@@ -2765,8 +2874,8 @@ async function 모델만바꾸기(session, cfg, 모델) {
2765
2874
  // 서버는 그대로지만 문은 같이 지난다. 등록 안 된 서버로 옮겨 가는 길도
2766
2875
  // 여기라서, 여기를 열어 두면 자물쇠에 구멍이 하나 남는다.
2767
2876
  if (!await 나가도되나묻기(session, p)) return;
2768
- if (!이미) { try { save(upsert(cfg, p)); } catch { /* 못 남겨도 이번 세션에는 바뀐다 */ } }
2769
- else { cfg.active = p.id; try { save(cfg); } catch {} }
2877
+ if (!이미) 설정남기기(upsert(cfg, p));
2878
+ else { cfg.active = p.id; 설정남기기(cfg); }
2770
2879
  연결적용(session, p);
2771
2880
  say(` ${mark.ok} 모델을 ${c.bold(모델)} 로 바꿨습니다. ${c.gray('서버는 그대로입니다.')}`);
2772
2881
  await 길이맞추기(session, cfg, p);
@@ -2788,7 +2897,7 @@ async function 길이맞추기(session, cfg, prof) {
2788
2897
  session.conn.ctx = 값;
2789
2898
  if (prof) {
2790
2899
  prof.ctx = 값;
2791
- try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
2900
+ 설정남기기(cfg);
2792
2901
  }
2793
2902
  say(` ${c.gray('컨텍스트')} ${c.white(값.toLocaleString())} ${c.gray('토큰 (' + fmtSize(값) + ') — ' + (r?.source ? r.source + '에서 읽음' : '서버가 안 알려줘 기본값'))}`);
2794
2903
  if (r?.max && r?.loaded && r.max > r.loaded) {
package/src/completion.js CHANGED
@@ -23,6 +23,10 @@
23
23
  */
24
24
  import { MODES } from './agent/modes.js';
25
25
  import { LEVELS, PROFILES } from './agent/effort.js';
26
+ // 승인 모드(auto·confirm·strict). agent/modes.js 의 MODES 와 헷갈리기 쉬운데,
27
+ // 저쪽은 `--work` 가 받는 '무슨 일을 하는 중인가' 다. 실제로 한 번 뒤바뀌어
28
+ // 있었다 — `--mode` 를 누르면 code·plan 이 나오고 `--work` 는 폴더를 냈다.
29
+ import { 차례 as 승인모드 } from './ui/approve.js';
26
30
 
27
31
  /** deel 이 받는 명령. bin/deel.js 의 switch 와 같아야 한다(검사가 지킨다). */
28
32
  export const 명령들 = [
@@ -39,6 +43,7 @@ export const 명령들 = [
39
43
  { 이름: 'scan', 뜻: '게이트웨이에 어떤 모델이 있나 훑는다', en: 'scan the gateway for available models' },
40
44
  { 이름: 'sessions', 뜻: '지난 대화 목록', en: 'list past conversations' },
41
45
  { 이름: 'ls', 뜻: 'sessions 와 같다', en: 'same as sessions' },
46
+ { 이름: 'reset', 뜻: '설정·기억·기록을 지운다 (그냥 치면 보여만 준다)', en: 'wipe settings, memory and records (bare: just shows)' },
42
47
  { 이름: 'completion', 뜻: '탭 완성 스크립트를 낸다', en: 'print a tab-completion script' },
43
48
  { 이름: 'version', 뜻: '판 번호', en: 'version number' },
44
49
  { 이름: 'help', 뜻: '도움말', en: 'help' },
@@ -55,8 +60,20 @@ export const 숨은명령 = ['-p'];
55
60
  */
56
61
  export const 깃발들 = [
57
62
  { 이름: '--root', 뜻: '작업 폴더', en: 'working folder', 값: '폴더' },
58
- { 이름: '--work', 뜻: '살림 폴더 (.deel 자리)', en: 'state folder (where .deel lives)', 값: '폴더' },
59
- { 이름: '--mode', 뜻: '무엇을 수 있게 할까', en: 'what the agent is allowed to do', 값: Object.keys(MODES) },
63
+ /*
64
+ * 둘은 뒤바뀌어 있었다.
65
+ *
66
+ * `--mode` 는 **승인 모드**(auto·confirm·strict)인데 작업 모드 목록이
67
+ * 붙어 있었고, `--work` 는 **작업 모드**인데 「살림 폴더」 라고 적혀 폴더를
68
+ * 완성해 주고 있었다. 그래서 `deel --mode <탭>` 을 누르면 받지도 않는
69
+ * code·plan 이 나왔다.
70
+ *
71
+ * 검사도 이걸 못 잡았다 — 완성 목록이 agent/modes.js 에서 온 것이 맞나만
72
+ * 봤기 때문이다. 베낀 자리가 맞는지는 봤는데, 베껴 온 곳이 맞는지는 안 봤다.
73
+ * 지금은 도움말에 적힌 값과 맞춰 본다 (test/completion.test.js).
74
+ */
75
+ { 이름: '--work', 뜻: '무슨 일을 하는 중인가', en: 'what kind of work this is', 값: Object.keys(MODES) },
76
+ { 이름: '--mode', 뜻: '무엇까지 물어보지 않고 할까', en: 'how much it may do without asking', 값: 승인모드 },
60
77
  { 이름: '--level', 뜻: '화면을 얼마나 자세히', en: 'how much detail on screen', 값: null },
61
78
  { 이름: '--ctx', 뜻: '창 크기 (예: 32k)', en: 'context window size (e.g. 32k)', 값: null },
62
79
  { 이름: '--max-tokens', 뜻: '한 번에 뱉을 최대 토큰', en: 'max tokens per reply', 값: null },
@@ -65,6 +82,7 @@ export const 깃발들 = [
65
82
  { 이름: '--online', 뜻: '묻지 않고 바깥으로 나간다', en: 'go outside without asking', 값: false },
66
83
  { 이름: '--offline', 뜻: '바깥으로 아예 안 나간다', en: 'never reach the network', 값: false },
67
84
  { 이름: '--yes', 뜻: '물어보지 않고 진행', en: 'do not ask, just proceed', 값: false },
85
+ { 이름: '--hard', 뜻: 'reset all 에서 되돌리기·감사기록까지', en: 'with reset all: undo snapshots and the audit log too', 값: false },
68
86
  { 이름: '--json', 뜻: '결과를 JSON 으로', en: 'output JSON', 값: false },
69
87
  { 이름: '--quiet', 뜻: '군말 없이', en: 'no chatter', 값: false },
70
88
  { 이름: '--tui', 뜻: '입력 상자를 켠다', en: 'force the input box on', 값: false },
package/src/config.js CHANGED
@@ -164,6 +164,23 @@ export function save(cfg, { toProject = false } = {}) {
164
164
  return p;
165
165
  }
166
166
 
167
+ /**
168
+ * 설정을 남긴다. 못 남겨도 던지지 않고, **왜 못 남겼는지를 돌려준다.**
169
+ *
170
+ * 부르는 자리가 아홉 군데인데 전부 `try { save(cfg) } catch {}` 였다. 「못
171
+ * 남겨도 이번 세션에는 먹는다」 는 맞는 말이지만, 그 뒤에 화면은 `✓` 를
172
+ * 찍는다 — 사람은 정해진 줄 알고 다음에 창을 열었다가 옛 값을 본다. 그리고
173
+ * 그때는 무엇 때문인지 알 길이 없다(홈이 읽기 전용인지, 디스크가 찼는지).
174
+ *
175
+ * 던지는 것은 안 바꾼다. 설정 하나 못 적었다고 대화가 끊기면 본말이 뒤집힌다.
176
+ *
177
+ * @returns {{ok: true, 자리: string} | {ok: false, 왜: string}}
178
+ */
179
+ export function 저장시도(cfg, 옵션 = {}) {
180
+ try { return { ok: true, 자리: save(cfg, 옵션) }; }
181
+ catch (err) { return { ok: false, 왜: err?.message ?? String(err) }; }
182
+ }
183
+
167
184
  export function activeProfile(cfg = load()) {
168
185
  if (!cfg.profiles.length) return null;
169
186
  return cfg.profiles.find((x) => x.id === cfg.active) ?? cfg.profiles[0];
@@ -172,6 +189,23 @@ export function activeProfile(cfg = load()) {
172
189
  // 환경변수가 있으면 파일보다 우선한다 — 사내망에서 키를 파일에 안 남기고 싶을 때 쓴다.
173
190
  const 푼것 = new Map(); // 잠긴 값 → 푼 글. 한 판에 한 번만 풀면 된다.
174
191
 
192
+ /**
193
+ * 잠근 열쇠를 못 푼 까닭. 화면이 한 번 읽어 가면 지워진다.
194
+ *
195
+ * ── 왜 있어야 하나 ──────────────────────────────────────────────────────
196
+ *
197
+ * keystore.js 의 풀기() 는 왜 못 풀었는지를 성실하게 만들어 돌려준다 —
198
+ * 「이 PC 의 이 계정에서 잠근 것만 풀립니다」 같은 말이다. 그 함수 머리말이
199
+ * 그렇게 하는 까닭까지 적어 뒀다: **빈 글자만 돌려주면 사람은 401 만 보고
200
+ * 게이트웨이를 의심한다.**
201
+ *
202
+ * 그런데 부르는 쪽이 그 말을 버리고 있었다. 빈 열쇠가 나가면 http.js 는
203
+ * Authorization 머리말을 아예 안 붙이고, 게이트웨이는 401 을 준다. 사람은
204
+ * 방화벽·주소·계정을 뒤진다. 진짜 까닭은 「설정 파일을 다른 PC 로 옮겼다」 인데.
205
+ */
206
+ let 열쇠탈 = null;
207
+ export function 열쇠탈소식() { const s = 열쇠탈; 열쇠탈 = null; return s; }
208
+
175
209
  export function resolveKey(profile) {
176
210
  const byName = profile?.id ? process.env[`DEEL_KEY_${profile.id.toUpperCase()}`] : null;
177
211
  const 값 = byName || process.env.DEEL_API_KEY || profile?.apiKey || '';
@@ -180,8 +214,19 @@ export function resolveKey(profile) {
180
214
  // 기다리는 시간이라, 판에 한 번만 풀고 들고 있는다.
181
215
  if (푼것.has(값)) return 푼것.get(값);
182
216
  const r = 풀기(값);
183
- 푼것.set(값, r.ok ? r.text : '');
184
- return r.ok ? r.text : '';
217
+ if (!r.ok) {
218
+ /*
219
+ * **실패는 안 담아 둔다.**
220
+ *
221
+ * 담아 두면 그 판 내내 다시 시도조차 안 한다. 사람이 그 사이에
222
+ * `deel setup` 으로 열쇠를 다시 넣어도 이 판에서는 영영 안 풀린다.
223
+ * 푸는 값이 0.2초라, 안 되는 것을 아껴서 얻을 것이 없다.
224
+ */
225
+ 열쇠탈 = r.why || '잠근 열쇠를 못 풀었습니다';
226
+ return '';
227
+ }
228
+ 푼것.set(값, r.text);
229
+ return r.text;
185
230
  }
186
231
 
187
232
  export function upsert(cfg, profile) {