deel-local-cli 1.20.7 → 1.20.9

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/src/commands.js CHANGED
@@ -29,7 +29,7 @@ import { 종, 알릴만한초 } from './ui/notify.js';
29
29
  import { 말, 말 as 옮긴말, 언어, 언어들, 언어정하기, 언어고르기, 옮긴만큼, 지시말, 지시말정하기, 지시말따로정했나 } from './i18n/index.js';
30
30
  import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기 } from './agent/models.js';
31
31
  import { allowTemporarily } from './safety/network.js';
32
- import { chat, 규격이름, 더할머리 } from './backend/adapter.js';
32
+ import { chat, 규격이름, 더할머리, 나가는눈금 } from './backend/adapter.js';
33
33
  import { 잠잠기본, 무소식기본 } from './backend/http.js';
34
34
  import { 인증서설정, 인증서말 } from './backend/clientcert.js';
35
35
  import { 알림채움, 알림말 } from './backend/retry.js';
@@ -64,7 +64,7 @@ export function 적용하기(고른것) {
64
64
  끔설정(값 === '끔');
65
65
  }
66
66
  import { PROFILES, LEVELS as THINK_LEVELS, normalizeProfile, table as effortTable, 가벼운강도, shiftLevel } from './agent/effort.js';
67
- import { 전선붙이기, 전선말, 눈금맞추기 } from './backend/wire.js';
67
+ import { 전선붙이기, 전선말 } from './backend/wire.js';
68
68
  import { scanLocal, toProfiles } from './backend/scan.js';
69
69
  import { list as listSessions } from './agent/store.js';
70
70
  import { MODES as WORK_MODES, ORDER as WORK_ORDER, DEFAULT as WORK_DEFAULT, normalize as normWork, get as getWork, canWrite, 보일이름, 보일한줄 } from './agent/modes.js';
@@ -110,7 +110,15 @@ function 경로처럼보이나(line) {
110
110
  if (첫낱말.includes('/') || 첫낱말.includes('\\')) return true;
111
111
  // `/tmp` 처럼 슬래시가 하나뿐이어도, 실제로 있는 자리면 경로로 본다.
112
112
  if (첫낱말 && !COMMANDS[첫낱말.toLowerCase()]) {
113
- try { if (existsSync(line.slice(1).trim())) return true; } catch { /* 못 보면 아닌 걸로 */ }
113
+ /*
114
+ * **적힌 그대로**도 보고, 앞 슬래시를 뗀 것도 본다.
115
+ *
116
+ * 앞서는 뗀 것만 봤다. 그러면 `/tmp` 가 지금 폴더의 `tmp` 를 찾다 못 찾아,
117
+ * 바로 위 머리글이 적어 둔 그 자리가 한 번도 안 먹었다. 둘 다 보면 여태
118
+ * 되던 것(지금 폴더의 이름)도 그대로 된다.
119
+ */
120
+ const 통째 = line.trim();
121
+ try { if (existsSync(통째) || existsSync(통째.slice(1))) return true; } catch { /* 못 보면 아닌 걸로 */ }
114
122
  }
115
123
  return false;
116
124
  }
@@ -504,7 +512,8 @@ export async function handle(line, session, ctx) {
504
512
  const 끄는말 = ['off', '끔', '꺼', '끄기', 'n', 'no', 'ㄴ'];
505
513
  const 값 = String(arg ?? '').trim().toLowerCase();
506
514
  if (!값) {
507
- const 켜져있나 = cfg.bell !== false;
515
+ // 지금 세션이 참이다. 설정 파일은 '다음에 켤 때' 값일 뿐이다.
516
+ const 켜져있나 = ctx?.종알림 ? ctx.종알림.켬 !== false : cfg.bell !== false;
508
517
  const 상태 = 켜져있나 ? c.hgreen(말('bell.stateOn')) : c.gray(말('bell.stateOff'));
509
518
  say('');
510
519
  say(` ${c.gray(말('bell.now', { 상태 }))}`);
@@ -522,6 +531,9 @@ export async function handle(line, session, ctx) {
522
531
  }
523
532
  cfg.bell = 켜는말.includes(값);
524
533
  설정남기기(cfg);
534
+ // 이 세션에도 바로 먹여야 한다 — 아래 줄이 그렇게 적는다.
535
+ // repl 은 켤 때 읽어 둔 그릇으로 울릴지 말지를 정한다(repl.js 의 알림).
536
+ if (ctx?.종알림) ctx.종알림.켬 = cfg.bell;
525
537
  say('');
526
538
  say(` ${mark.ok} ${말(cfg.bell ? 'bell.turnedOn' : 'bell.turnedOff')}`);
527
539
  // 켤 때는 한 번 울려 준다. "켰다는데 소리가 나나?" 를 그 자리에서 확인하게.
@@ -726,7 +738,9 @@ export async function handle(line, session, ctx) {
726
738
  */
727
739
  if (/^(auto|자동)(\s|$)/.test(말)) {
728
740
  const 값 = 말.replace(/^(auto|자동)\s*/, '').trim().toLowerCase();
729
- const 켤까 = === '' ? session.autoThink === false : /^(on|켜|켜기|true)$/.test(값);
741
+ // `켬` 켜는 말이다. /bell 받는데 여기만 빠져 있어서, `/think auto 켬`
742
+ // 이 **끄는** 명령이 됐다 — 켜려고 친 말로 꺼지고 화면은 「껐습니다」 였다.
743
+ const 켤까 = 값 === '' ? session.autoThink === false : /^(on|켬|켜|켜기|true|y|yes|ㅇ)$/.test(값);
730
744
  session.autoThink = 켤까;
731
745
  say(` ${mark.ok} ${옮긴말(켤까 ? 'think.autoOn' : 'think.autoOff')}`);
732
746
  showThink(session);
@@ -745,6 +759,12 @@ export async function handle(line, session, ctx) {
745
759
  }
746
760
 
747
761
  if (!THINK_LEVELS.includes(말)) {
762
+ // 못 알아들은 값은 못 알아들었다고 먼저 말한다. 안 그러면 `/think hgih`
763
+ // 가 오류 없이 표만 띄워서, 강도를 올린 줄 알고 그대로 쓰게 된다.
764
+ if (말) {
765
+ say('');
766
+ say(` ${mark.no} ${c.gray(옮긴말('think.unknown', { 값: 말 }))}`);
767
+ }
748
768
  showThink(session);
749
769
  return { handled: true };
750
770
  }
@@ -769,7 +789,16 @@ export async function handle(line, session, ctx) {
769
789
  */
770
790
  case 'mode': {
771
791
  const { 승인, 차례: 승인차례, 표시: 승인표시 } = await import('./ui/approve.js');
772
- if (!MODES[arg]) {
792
+ /*
793
+ * 표에 **제 것으로 있는** 이름만 받는다.
794
+ *
795
+ * `MODES[arg]` 는 물려받은 열쇠에도 참이다. `/mode constructor` 를
796
+ * 치면 목록으로 안 빠지고 그대로 session.mode 에 들어가는데, 그 값은
797
+ * strict 도 confirm 도 아니라 **아무것도 안 물어보는** 상태가 된다
798
+ * (agent/loop.js 의 needsOk). 화면에는 `엄격 → undefined` 가 찍힌다.
799
+ * `--work` 오타가 가장 센 모드로 돌던 것과 같은 자리다.
800
+ */
801
+ if (!Object.hasOwn(MODES, arg)) {
773
802
  rule('승인 방식 — 무엇을 물어볼까', 70);
774
803
  for (const k of 승인차례) {
775
804
  const 지금 = k === session.mode;
@@ -820,7 +849,23 @@ export async function handle(line, session, ctx) {
820
849
  * 되돌린 것이 다음 순간 되살아난다.
821
850
  */
822
851
  case 'undo': {
823
- const n = Math.max(1, parseInt(arg, 10) || 1);
852
+ /*
853
+ * 못 읽는 인자로 **말없이 한 턴을 되돌리지 않는다.**
854
+ *
855
+ * parseInt 는 `다섯`·`abc` 를 NaN, `-2` 를 음수로 준다. 앞서는 둘 다
856
+ * 1 로 깎아서, 다섯 턴을 되돌리려던 사람이 한 턴만 되돌아간 화면을
857
+ * 봤다. 되돌리기는 **파일을 실제로 되돌리는** 명령이라 잘못 읽은 수로
858
+ * 도는 값이 다른 명령보다 비싸다. /work 가 오타를 먼저 말하는 것과
859
+ * 같은 잣대다.
860
+ */
861
+ const 준말 = String(arg ?? '').trim();
862
+ if (준말 && !/^[0-9]+$/.test(준말)) {
863
+ say(` ${mark.no} ${c.gray('몇 턴을 되돌릴지 숫자로 적어 주세요.')} ${c.cyan('/undo 3')}`);
864
+ say(` ${c.gray('그냥')} ${c.cyan('/undo')} ${c.gray('만 치면 한 턴을 되돌립니다.')}`);
865
+ say('');
866
+ return { handled: true };
867
+ }
868
+ const n = Math.max(1, parseInt(준말, 10) || 1);
824
869
  const r = ctx.history.undo(n);
825
870
  /*
826
871
  * 세는 수는 **진짜로 되돌아간 것**이다.
@@ -832,8 +877,17 @@ export async function handle(line, session, ctx) {
832
877
  */
833
878
  const 되돌린수 = r.되돌린수 ?? r.restored.length;
834
879
  ctx.audit.undo({ turns: r.turns, files: 되돌린수 });
835
- if (!r.restored.length) {
880
+ /*
881
+ * **하나도 안 되돌아갔으면** 되돌렸다고 하지 않는다.
882
+ *
883
+ * restored 에는 못 되돌린 것과 손대지 않은 것(바이너리처럼 내용을 못
884
+ * 떠 둔 파일)도 들어 있다. 그 길이만 보면, 되돌아간 파일이 0개인
885
+ * 판에도 `✓ … 파일 0개를 되돌렸습니다` 를 찍고 그 밑에 못 되돌린
886
+ * 파일 목록을 성공 목록처럼 늘어놓았다.
887
+ */
888
+ if (!되돌린수) {
836
889
  say(` ${c.gray(말('undo.nothing'))}`);
890
+ if (r.못한것?.length) say(` ${mark.warn} ${말('undo.failed', { n: r.못한것.length })}`);
837
891
  say('');
838
892
  return { handled: true };
839
893
  }
@@ -1238,6 +1292,33 @@ export async function handle(line, session, ctx) {
1238
1292
  return { handled: true };
1239
1293
  }
1240
1294
 
1295
+ /*
1296
+ * 지우려다 **적고 마는** 자리를 막는다.
1297
+ *
1298
+ * `/memory 지우기` 처럼 번호를 안 붙이면 위 무늬에 안 걸리고 아래
1299
+ * 「이걸 기억해라」 로 떨어졌다. 그래서 `지우기` 라는 낱말이 기억 파일에
1300
+ * **영영 적히고**, 화면에는 「기억했습니다」 라고 떴다. 그 줄은 그 뒤로
1301
+ * 매 요청마다 모델에게 같이 나간다. 지우려던 사람이 쓰레기를 하나 더
1302
+ * 심는 꼴이고, 그것도 성공 표시를 보면서 그렇게 된다.
1303
+ */
1304
+ // 낱말 끝은 `\\b` 로 못 잡는다 — 한글은 \\w 가 아니라 `지우기` 뒤에 낱말
1305
+ // 경계가 안 생긴다. 실제로 이 무늬로 고쳤다가 한글만 그대로 새 나갔다.
1306
+ if (/^(지우기|잊어|forget|rm)(\s|$)/i.test(말)) {
1307
+ const 있는것 = M.읽기(session.root).줄들;
1308
+ say(` ${mark.no} ${c.gray('몇 번째 줄을 지울지 번호를 적어 주세요.')}`);
1309
+ if (있는것.length) {
1310
+ for (const [i, l] of 있는것.entries()) {
1311
+ say(` ${c.gray(String(i + 1).padStart(2))} ${c.white(clip(l, 82))}`);
1312
+ }
1313
+ say('');
1314
+ say(` ${c.cyan('/memory 지우기 1')} ${c.gray('한 줄만')} ${c.cyan('/memory 비우기')} ${c.gray('전부')}`);
1315
+ } else {
1316
+ say(` ${c.gray('지금은 기억해 둔 것이 없습니다.')}`);
1317
+ }
1318
+ say('');
1319
+ return { handled: true };
1320
+ }
1321
+
1241
1322
  // 그 밖의 말은 '이걸 기억해라' 로 본다. 모델을 안 거치고 바로 적는 길이다.
1242
1323
  if (말) {
1243
1324
  const r = M.더하기(session.root, 말);
@@ -1745,12 +1826,23 @@ function showSkills(session, arg) {
1745
1826
  }
1746
1827
  if (q.startsWith('on ')) {
1747
1828
  const term = q.slice(3).trim().toLowerCase();
1748
- let n = 0;
1749
- for (const s of all) {
1750
- s.enabled = 낮게(s.name).includes(term) || 낮게(s.description).includes(term);
1751
- if (s.enabled) n++;
1829
+ const 걸린것 = all.filter((s) => 낮게(s.name).includes(term) || 낮게(s.description).includes(term));
1830
+ /*
1831
+ * 개도 걸리면 **아무것도 안 건드린다.**
1832
+ *
1833
+ * 앞서는 걸림 여부를 그대로 enabled 에 넣었다. 그래서 오타 하나면 전부
1834
+ * false 가 되고, 화면에는 `✓ "리뷰" 에 걸리는 0개만 올립니다` 가 떴다.
1835
+ * 올리려고 친 명령이 가지고 있던 것까지 다 내려 버리는데, 표시는 ✓ 다.
1836
+ * 그다음 모델이 스킬을 못 쓰는 것을 보고 사람은 스킬이 깨진 줄 안다.
1837
+ */
1838
+ if (!걸린것.length) {
1839
+ say(` ${mark.no} "${term}" 에 걸리는 스킬이 없습니다 ${c.gray('— 올라간 것은 그대로 둡니다.')}`);
1840
+ say(` ${c.gray('무엇이 있는지 보려면')} ${c.cyan('/skills')}`);
1841
+ say('');
1842
+ return;
1752
1843
  }
1753
- say(` ${mark.ok} "${term}" 걸리는 ${n}개만 올립니다.`);
1844
+ for (const s of all) s.enabled = 걸린것.includes(s);
1845
+ say(` ${mark.ok} "${term}" 에 걸리는 ${걸린것.length}개만 올립니다.`);
1754
1846
  say('');
1755
1847
  return;
1756
1848
  }
@@ -1934,7 +2026,15 @@ function showThink(session, { 자세히 = false } = {}) {
1934
2026
  }
1935
2027
  if (session.conn?.전선) {
1936
2028
  const 카드 = session.conn.전선;
1937
- const 나가는것 = 눈금맞추기(카드, session.think);
2029
+ /*
2030
+ * **정말로 나가는 것만** 적는다.
2031
+ *
2032
+ * 여태 여기서 `눈금맞추기()` 를 직접 불렀다. 그 함수는 카드가 아는 눈금으로
2033
+ * 낮추는 일만 하고 그 눈금이 실릴 칸이 있는지는 모른다 — 그래서 눈금이
2034
+ * 아예 안 나가는 전선(budget·boolean·none)에서도 「지금 나가는 값 high」
2035
+ * 가 떴다. 몸을 만드는 자리와 같은 함수를 쓴다(backend/adapter.js).
2036
+ */
2037
+ const 나가는것 = 나가는눈금(session.conn.kind, 카드, session.think);
1938
2038
  /*
1939
2039
  * ── 「끈다」 가 정말로 꺼지는 창구인가 ────────────────────────────
1940
2040
  *
@@ -2287,9 +2387,18 @@ async function 리뷰명령(session, ctx) {
2287
2387
 
2288
2388
  async function 커밋명령(session, ctx, arg = '') {
2289
2389
  const 말한것 = String(arg ?? '').trim();
2290
- const 전부 = /^(전부|all)$/i.test(말한것);
2291
- const 미리보기 = /^(미리보기|preview|dry|--dry-run)$/i.test(말한것);
2292
- const 준제목 = 전부 || 미리보기 ? null : (말한것 || null);
2390
+ /*
2391
+ * `전부` 뒤에 제목을 같이 적을 수 있다.
2392
+ *
2393
+ * 앞서는 `^(전부|all)$` 라 `/commit 전부 버그 수정` 이 **둘 다** 어긋났다 —
2394
+ * 전부가 거짓이 되어 폴더째 바뀐 파일이 빠지고, `전부 버그 수정` 이 통째로
2395
+ * 커밋 제목이 됐다. 어느 쪽도 화면에 안 적힌다.
2396
+ */
2397
+ const 전부친것 = /^(전부|all)(\s+|$)/i.exec(말한것);
2398
+ const 전부 = !!전부친것;
2399
+ const 남은말 = 전부 ? 말한것.slice(전부친것[0].length).trim() : 말한것;
2400
+ const 미리보기 = /^(미리보기|preview|dry|--dry-run)$/i.test(남은말);
2401
+ const 준제목 = 미리보기 ? null : (남은말 || null);
2293
2402
 
2294
2403
  say('');
2295
2404
  rule('커밋', 70);
@@ -2516,6 +2625,37 @@ function 배움명령(session, ctx, arg = '') {
2516
2625
  return;
2517
2626
  }
2518
2627
 
2628
+ /*
2629
+ * ── 전선만 지우는 길 ────────────────────────────────────────────────────
2630
+ *
2631
+ * 전선 모양은 틀리게 배울 수 있는 유일한 것이고(backend/wire.js), 틀리면
2632
+ * 멀쩡한 기능이 꺼진 채 굳는다. 그런데 되돌릴 길이 「전부 비우기」 하나라,
2633
+ * 몇 주 쌓은 명령 겪음까지 같이 날아갔다 — 그게 아까워서 사람은 안 비우고,
2634
+ * 안 비우니 꺼진 기능을 그냥 안고 쓴다. 전선만 지우면 잃을 것이 없다.
2635
+ */
2636
+ /*
2637
+ * 지우는 말은 **반드시 적어야** 한다.
2638
+ *
2639
+ * 앞서는 `\s*(…)?` 라 `/learned 전선` 만 쳐도 곧바로 지웠다. 화면이 알려
2640
+ * 주는 명령은 `/learned 전선 지우기` 라 그 짧은 꼴은 어디에도 안 적혀
2641
+ * 있는데, 「전선에서 뭘 배웠나 보자」 는 마음으로 치면 그것이 지우는
2642
+ * 명령이 됐다. 지우는 명령은 지우겠다고 적었을 때만 지운다.
2643
+ */
2644
+ if (/^(전선|wire)$/i.test(String(arg).trim())) {
2645
+ say(` ${c.gray('전선에서 배운 것만 비우려면')} ${c.cyan('/learned 전선 지우기')}`);
2646
+ say(` ${c.gray('쌓인 것을 보려면')} ${c.cyan('/learned')}`);
2647
+ say('');
2648
+ return;
2649
+ }
2650
+ if (/^(전선|wire)\s+(지우기|clear|forget|비우기)$/i.test(String(arg).trim())) {
2651
+ 배움.지우기('전선');
2652
+ if (session.conn) 전선붙이기(session.conn, 배움);
2653
+ say(` ${mark.ok} ${c.gray('전선에서 배운 것만 비웠습니다 — 다음 요청부터 짐작으로 다시 시작합니다.')}`);
2654
+ say(` ${c.gray('겪어 본 명령과 토큰 보정은 그대로 둡니다.')}`);
2655
+ say('');
2656
+ return;
2657
+ }
2658
+
2519
2659
  if (/^(지우기|clear|forget|비우기)$/i.test(String(arg).trim())) {
2520
2660
  배움.지우기('전부');
2521
2661
  session.배움요약 = null;
@@ -2580,8 +2720,29 @@ function 배움명령(session, ctx, arg = '') {
2580
2720
  } else {
2581
2721
  say(` ${c.gray('아직 프롬프트에 실을 만큼 확실한 것은 없습니다 — 두 번 이상 겪어야 싣습니다.')}`);
2582
2722
  }
2723
+ /*
2724
+ * ── 전선에서 배운 것도 여기 보인다 ──────────────────────────────────────
2725
+ *
2726
+ * 쌓인 것 중 **제일 크게 작용하는 것**이 전선 모양인데(무엇을 실어 보낼지
2727
+ * 가 여기서 갈린다) 이 화면에 아예 안 떴다. 그래서 잘못 배운 판에서 사람이
2728
+ * 「배운 것을 보자」 고 여기를 열면, 정작 문제인 줄만 안 보였다.
2729
+ */
2730
+ const 전선 = session.conn?.전선;
2731
+ if (전선) {
2732
+ const 배운칸 = 전선.배운칸 ?? [];
2733
+ say(` ${c.bold('이 창구에 실어 보내는 모양')}`);
2734
+ say(` ${c.gray(전선말(전선))}`);
2735
+ if (배운칸.length) {
2736
+ say(` ${c.gray('「배움」 은 짐작이 아니라 서버가 거절해서 고친 자리입니다 — 틀릴 수도 있습니다.')}`);
2737
+ }
2738
+ say('');
2739
+ }
2740
+
2583
2741
  say('');
2584
2742
  say(` ${c.gray('지우려면')} ${c.cyan('/learned 지우기')}`);
2743
+ if (전선?.배운칸?.length) {
2744
+ say(` ${c.gray('전선에서 배운 것만 되돌리려면')} ${c.cyan('/learned 전선 지우기')}`);
2745
+ }
2585
2746
  say('');
2586
2747
  }
2587
2748
 
@@ -2792,7 +2953,7 @@ function 모델급(session, arg = '') {
2792
2953
  return;
2793
2954
  }
2794
2955
 
2795
- if (값 && 별명[]) {
2956
+ if (값 && Object.hasOwn(별명, 값)) {
2796
2957
  session.급정한것 = 별명[값];
2797
2958
  session.급다시재기?.();
2798
2959
  const v = session.급값();
@@ -2803,6 +2964,19 @@ function 모델급(session, arg = '') {
2803
2964
  return;
2804
2965
  }
2805
2966
 
2967
+ /*
2968
+ * 못 알아들은 값은 **못 알아들었다고 말한다.**
2969
+ *
2970
+ * 앞서는 `/grade 크게` 같은 오타가 오류 한 줄 없이 지금 상태 표로
2971
+ * 떨어졌다. 사람은 정해진 줄로 읽고 그대로 쓴다. 바로 옆 `/work` 는
2972
+ * 「그런 모드는 없습니다」 를 먼저 말한다 — 잣대를 같게 맞춘다.
2973
+ */
2974
+ if (값) {
2975
+ say('');
2976
+ say(` ${mark.no} ${c.gray('그런 급은 없습니다:')} ${c.white(값)}`);
2977
+ say(` ${c.gray('고를 수 있는 것:')} ${c.cyan('작음')} ${c.gray('·')} ${c.cyan('보통')} ${c.gray('·')} ${c.cyan('큼')} ${c.gray('·')} ${c.cyan('auto')}`);
2978
+ }
2979
+
2806
2980
  // 인자가 없으면 지금 상태를 보여 준다.
2807
2981
  const g = session.급();
2808
2982
  const v = session.급값();
@@ -3209,8 +3383,17 @@ async function 모델만바꾸기(session, cfg, 모델, ctx = null) {
3209
3383
  // 서버는 그대로지만 문은 같이 지난다. 등록 안 된 서버로 옮겨 가는 길도
3210
3384
  // 여기라서, 여기를 열어 두면 자물쇠에 구멍이 하나 남는다.
3211
3385
  if (!await 나가도되나묻기(session, p)) return;
3212
- if (!이미) 설정남기기(upsert(cfg, p));
3213
- else { cfg.active = p.id; 설정남기기(cfg); }
3386
+ /*
3387
+ * 새로 만든 프로필도 **지금 쓰는 것**으로 못 박는다.
3388
+ *
3389
+ * upsert 는 active 가 비어 있을 때만 채운다. 여기서는 이미 차 있으므로,
3390
+ * 새 프로필을 넣기만 하고 active 는 옛 모델에 그대로 남았다 — 화면에는
3391
+ * 「모델을 X 로 바꿨습니다」 라고 적히고, 다음에 켜면 옛 모델로 돌아온다.
3392
+ * 이미 있는 프로필로 갈아탈 때(아래 갈래)는 제대로 하고 있었다.
3393
+ */
3394
+ if (!이미) upsert(cfg, p);
3395
+ cfg.active = p.id;
3396
+ 설정남기기(cfg);
3214
3397
  연결적용(session, p, ctx);
3215
3398
  say(` ${mark.ok} 모델을 ${c.bold(모델)} 로 바꿨습니다. ${c.gray('서버는 그대로입니다.')}`);
3216
3399
  await 길이맞추기(session, cfg, p);
package/src/i18n/en.js CHANGED
@@ -42,6 +42,7 @@ export const en = {
42
42
  // ── /think screen ──────────────────────────────────────────────────
43
43
  'run.noTools': 'Tool calling is unconfirmed — check with deel diagnose',
44
44
  'think.effort': 'Effort',
45
+ 'think.unknown': 'No such effort level: {값}',
45
46
  'think.profile': 'Profile',
46
47
  'think.detail': 'detail',
47
48
  'think.harder': 'harder',
@@ -98,7 +99,7 @@ export const en = {
98
99
  'cmd.commit.desc': 'commit — stages only what this session changed; message from the diff and the evidence',
99
100
  'cmd.review.desc': 'review the changes in a fresh context — reports findings only, changes nothing',
100
101
  'cmd.paste.desc': 'attach the screenshot sitting in the clipboard (capture, then /paste)',
101
- 'cmd.commit.arg': '[all|preview|title]',
102
+ 'cmd.commit.arg': '[all] [title|preview]',
102
103
  'cmd.sessions.desc': 'past sessions in this folder',
103
104
  'cmd.recall.desc': 'search past sessions — by what was said, not by title',
104
105
  'cmd.recall.arg': '<text>',
@@ -542,6 +543,7 @@ export const en = {
542
543
  'wire.cacheMarked': 'marked',
543
544
  'wire.cacheKey': 'key',
544
545
  'wire.cacheAuto': 'server-side',
546
+ 'wire.learned': 'learned',
545
547
  'cost.cache': 'Cache',
546
548
  'cost.think': 'Thinking',
547
549
  'cost.thinkLine': '{토큰} tokens · {몫}% of output',
package/src/i18n/ko.js CHANGED
@@ -31,6 +31,7 @@ export const ko = {
31
31
  // 단계 이름·배분 이름은 agent/effort.js 가 들고 있다 — modes.js 와 같은 방식.
32
32
  'run.noTools': '도구 호출이 확인되지 않았습니다 — deel diagnose 로 점검하세요',
33
33
  'think.effort': '추론 강도',
34
+ 'think.unknown': '그런 강도는 없습니다: {값}',
34
35
  'think.profile': '배분',
35
36
  'think.detail': '자세히',
36
37
  'think.harder': '더 세게',
@@ -87,7 +88,7 @@ export const ko = {
87
88
  'cmd.commit.desc': '커밋 — 이번 대화가 바꾼 것만 담고, diff 와 증거로 메시지를 짓는다',
88
89
  'cmd.review.desc': '바꾼 것을 새 창에서 한 번 더 본다 — 찾은 것만 말하고 아무것도 안 고친다',
89
90
  'cmd.paste.desc': '클립보드에 든 화면 캡처를 붙인다 (캡처 → /paste)',
90
- 'cmd.commit.arg': '[전부|미리보기|제목]',
91
+ 'cmd.commit.arg': '[전부] [제목|미리보기]',
91
92
  'cmd.sessions.desc': '이 폴더의 지난 대화 목록',
92
93
  'cmd.recall.desc': '지난 대화에서 찾기 — 목록 말고 내용으로',
93
94
  'cmd.recall.arg': '<찾을 말>',
@@ -589,6 +590,7 @@ export const ko = {
589
590
  'wire.cacheMarked': '표식',
590
591
  'wire.cacheKey': '열쇠',
591
592
  'wire.cacheAuto': '서버자동',
593
+ 'wire.learned': '배움',
592
594
  'cost.cache': '캐시',
593
595
  'cost.think': '생각',
594
596
  'cost.thinkLine': '{토큰} 토큰 · 답의 {몫}%',
package/src/repl.js CHANGED
@@ -1188,6 +1188,16 @@ export async function chatLoop(opts = {}) {
1188
1188
  loadedSkills: new Set(),
1189
1189
  // 고친 뒤 진단을 볼지. /lsp off 로 끈다 (lsp/diag.js).
1190
1190
  lsp: { 켬: true },
1191
+ /*
1192
+ * 종을 울릴지. `/bell on|off` 가 **이 그릇**을 고친다.
1193
+ *
1194
+ * 앞서는 명령이 설정 파일만 고쳤다. 울릴지 말지는 켤 때 한 번 읽어
1195
+ * 둔 값(알림.켬)이 정하므로, `/bell off` 를 쳐도 그 세션 내내 계속
1196
+ * 울렸다 — 화면은 「이 세션에도 바로 먹습니다」 라고 적으면서.
1197
+ * `/bell on` 은 더 나빴다: 그 자리에서 한 번 울려 켜졌다는 증거까지
1198
+ * 보여 준 다음, 다음 턴부터 안 울렸다.
1199
+ */
1200
+ 종알림: 알림,
1191
1201
  ask,
1192
1202
  // 암호는 여기서만 받는다. 받은 값은 도구가 쓰고 버린다 —
1193
1203
  // 설정에도, 세션 기록에도, 감사기록에도, 명령줄에도 안 남는다.
@@ -2279,8 +2289,9 @@ export async function chatLoop(opts = {}) {
2279
2289
 
2280
2290
  case 'images_folded': {
2281
2291
  clearThinking();
2282
- const 장수 = (ev.뺀것들 ?? []).reduce((a2, x) => a2 + x.장수, 0);
2283
- say(` ${c.cyan('◲')} ${c.gray(`오래된 그림 ${장수}장을 뺐습니다 사람이 말은 그대로입니다.`)}`);
2292
+ // 장수는 접는 쪽이 세어 준다 (agent/compact.js foldImages).
2293
+ // 여기서 세면 한쪽만 고쳐지고, 그때부터 화면이 다른 수를 적는다.
2294
+ say(` ${c.cyan('◲')} ${c.gray(`오래된 그림 ${ev.뺀것}장을 뺐습니다 — 사람이 쓴 말은 그대로입니다.`)}`);
2284
2295
  break;
2285
2296
  }
2286
2297
 
package/src/tools/jobs.js CHANGED
@@ -384,6 +384,35 @@ class 일감 {
384
384
  while (this.바이트 > 출력상한 && this.조각들.length > 1) {
385
385
  this.바이트 -= this.조각들.shift().length;
386
386
  }
387
+ /*
388
+ * 한 조각이 **혼자** 상한을 넘으면 그 조각의 뒤만 남긴다.
389
+ *
390
+ * 위 되풀이는 조각이 둘 이상일 때만 버린다. 그래서 한 조각이 혼자 넘치면
391
+ * **아무것도 못 버리는데 아래에서 잘렸다고 적는다.** 그 사이에 읽으면
392
+ * 멀쩡한 글에 "앞이 잘렸습니다" 가 붙고 읽은 자리도 0 으로 돌아간다 —
393
+ * 모델은 없는 앞을 찾으러 처음부터 다시 읽는다. (다음 조각이 들어오면
394
+ * 그때는 둘이 되어 버려지므로, 거짓말은 그 한 판만이다. 그래도 그 한
395
+ * 판이 되풀이를 부르는 자리다 — 읽기() 머리말 참고.)
396
+ *
397
+ * 그리고 버릴 수 있게 만들어 두는 것이 맞다. 상한 위에 남겨 두면 그
398
+ * 다음 조각 하나에 256KB 가 통째로 날아간다.
399
+ *
400
+ * 베껴 담는다. subarray 는 원래 덩이를 그대로 붙잡고 있어서, 큰 조각
401
+ * 하나가 들어온 판에서는 놓으려는 그 메모리가 안 놓인다.
402
+ */
403
+ if (this.바이트 > 출력상한 && this.조각들.length === 1) {
404
+ const 하나 = this.조각들[0];
405
+ this.조각들[0] = Buffer.from(하나.subarray(하나.length - 출력상한));
406
+ this.바이트 = this.조각들[0].length;
407
+ }
408
+ /*
409
+ * 여기 오면 **반드시 무엇인가 버렸다** — 그래서 「안 버렸으면」 갈래를
410
+ * 두지 않는다. 위에서 상한을 넘은 것을 확인했고, 조각이 둘 이상이면
411
+ * 되풀이가 버리고 하나뿐이면 그 하나를 줄였다. 늘 참인 조건을 남기면
412
+ * 어긋내기가 「그 줄을 지워도 아무 검사도 안 깨진다」 고 말하는데, 그건
413
+ * 맞는 말이라 고칠 수도 없다 — 대신 읽는 사람은 안 버리는 길이 따로
414
+ * 있는 줄로 읽는다.
415
+ */
387
416
  this.앞잘림 = true;
388
417
  this.막버렸다 = true;
389
418
  this.읽은글자 = 0;
@@ -516,10 +545,24 @@ export async function 띄우기(명령, { cwd, 설명 = null, 기다림 = 1500,
516
545
 
517
546
  kid.stdout?.on('data', (b) => j.담기(b));
518
547
  kid.stderr?.on('data', (b) => j.담기(b));
519
- kid.on('error', (err) => { j.담기(Buffer.from(`\n[띄우기 실패: ${err.message}]\n`)); j.상태 = '끝남'; j.종료코드 = -1; });
548
+ kid.on('error', (err) => {
549
+ j.담기(Buffer.from(`\n[띄우기 실패: ${err.message}]\n`));
550
+ j.상태 = '끝남';
551
+ j.종료코드 = -1;
552
+ // 왜 표를 세우나 — 바로 아래 close 머리말.
553
+ j.띄우기실패 = true;
554
+ });
520
555
  kid.on('close', (code, sig) => {
521
556
  if (j.상태 === '죽임') return;
522
557
  j.상태 = '끝남';
558
+ /*
559
+ * 띄우기 자체가 실패한 판은 **덮지 않는다.**
560
+ *
561
+ * 명령이 없으면(ENOENT) Node 는 error 를 낸 다음 close 를 code=null 로
562
+ * 부른다. 그러면 위에서 세워 둔 -1 이 null 로 덮이고, null 은 이 자리에서
563
+ * 「시그널로 죽었다」 와 구별이 안 된다. 안 뜬 까닭이 종료코드에서 지워진다.
564
+ */
565
+ if (j.띄우기실패) { 끝난것치우기(); return; }
523
566
  j.종료코드 = sig ? null : code;
524
567
  j.시그널 = sig ?? null;
525
568
  // 끝난 것이 쌓이지 않게. 도는 것은 최대일감 이 따로 막는다.
@@ -628,7 +671,17 @@ export async function 끝내기(번호) {
628
671
  */
629
672
  await 죽는말기다리기(j.kid, 죽는말기다림);
630
673
  }
631
- 파이프끊기(j.kid);
674
+ /*
675
+ * ── 안 죽은 놈의 파이프는 **끊지 않는다** ──────────────────────────────
676
+ *
677
+ * 여기서 늘 끊고 있었다. 그런데 바로 아래에서 「그러고도 안 죽었으면 목록에
678
+ * 남긴다 — 다시 시킬 수 있어야 한다」 고 한다. 파이프를 이미 끊어 놓고
679
+ * 남기면, 목록에는 `도는중` 으로 뜨는데 그놈이 뱉는 말은 다시는 안 들어온다.
680
+ * 사람은 화면을 보며 기다리고, 자식은 다음 쓰기에서 EPIPE 를 맞는다.
681
+ * 남겨 두기로 한 까닭이 「다시 볼 수 있게」 인데 정작 볼 길을 끊던 셈이다.
682
+ */
683
+ const 죽었나 = !아직사나(j.kid);
684
+ if (죽었나) 파이프끊기(j.kid);
632
685
  const 남은 = j.읽기({});
633
686
  /*
634
687
  * ★ 그러고도 안 죽었으면 **그렇다고 말한다.**
@@ -640,7 +693,7 @@ export async function 끝내기(번호) {
640
693
  *
641
694
  * 안 죽었으면 목록에 남긴다. 그래야 사람이 한 번 더 시킬 수 있다.
642
695
  */
643
- if (아직사나(j.kid)) {
696
+ if (!죽었나) {
644
697
  j.상태 = '도는중'; // 지우지 않는다 — 다시 시킬 수 있어야 한다
645
698
  return { 이미: false, 안죽음: true, 명령: j.명령, 초: j.산햇수(), 남은: 남은.글 };
646
699
  }
@@ -723,6 +776,8 @@ export function 일감인자(args) {
723
776
  };
724
777
  const 날번호 = 하나씩('번호');
725
778
  return {
779
+ // 날것을 같이 내놓는다 — 숫자가 아닐 때 **무엇을 받았는지** 적어야 한다.
780
+ 날번호,
726
781
  번호: 날번호 == null ? null : Number(날번호),
727
782
  끝내기: !!하나씩('끝내기'),
728
783
  처음부터: !!하나씩('처음부터'),
@@ -791,11 +846,34 @@ export const JOBS_TOOL = {
791
846
  + ' 번호 없이 부르면 목록입니다.',
792
847
  };
793
848
  }
849
+ /*
850
+ * 몇 개만 못 알아들은 판도 **말해 준다.**
851
+ *
852
+ * 위 갈래는 「하나도」 못 알아들었을 때만 걸린다. 그래서
853
+ * `{job: 1, action: 'stop'}` 은 걸리지 않고 — `action` 을 조용히 버린 뒤
854
+ * 출력만 읽어 **성공으로** 돌려줬다. 끄라고 시킨 모델은 껐다고 여기고
855
+ * 넘어가는데 서버는 그대로 돈다. 위 머리말이 없애겠다고 적어 둔 바로 그
856
+ * 고장이, 인자를 하나 더 붙인 판에 그대로 남아 있었다 (34차 리뷰).
857
+ *
858
+ * 오류로 막지는 않는다 — 알아들은 것은 하라고 시킨 것이 맞다. 대신 버린
859
+ * 이름을 적어 보내서, 뜻이 거기 있었으면 다음 걸음에서 고쳐 부르게 한다.
860
+ */
861
+ const 버린말 = 받은것.모르는것.length
862
+ ? `\n(못 알아들어 버린 인자: ${받은것.모르는것.join(', ')} — Jobs 는 job · stop · from_start 만 받습니다.`
863
+ + ' 끝내려던 것이면 stop: true 로 다시 불러라.)'
864
+ : '';
794
865
  const 번호 = 받은것.번호;
795
866
  // 숫자가 아닌 것을 받으면 NaN 이 되고, 그대로 두면 `NaN번 일감이 없습니다`
796
867
  // 라는 말이 모델에게 나간다. 무엇이 잘못됐는지 안 알려 주는 오류다.
797
868
  if (번호 != null && !Number.isFinite(번호)) {
798
- return { error: `번호는 숫자여야 합니다 (받은 것: ${JSON.stringify(args?.번호)}). 번호 없이 Jobs 를 불러 목록을 보세요.` };
869
+ /*
870
+ * 받은 것을 **실제로 읽은 그 칸**에서 적는다.
871
+ *
872
+ * 여기가 `args?.번호` 였다. 설명서에 실린 이름은 `job` 이라 모델은 거의
873
+ * 늘 `job` 으로 보내는데, 그러면 이 말은 `(받은 것: undefined)` 가 된다.
874
+ * 무엇을 잘못 보냈는지 알려 주려고 있는 줄이 정작 그것만 안 알려 줬다.
875
+ */
876
+ return { error: `번호는 숫자여야 합니다 (받은 것: ${JSON.stringify(받은것.날번호)}). 번호 없이 Jobs 를 불러 목록을 보세요.` };
799
877
  }
800
878
 
801
879
  if (번호 == null) {
@@ -830,7 +908,7 @@ export const JOBS_TOOL = {
830
908
  * 그러면 포트가 물려서 안 뜨고, 왜 안 되는지도 모른다.
831
909
  */
832
910
  if (치운것) 줄들.push(` (오래된 것 ${치운것}개는 자리를 위해 지웠습니다)`);
833
- return { content: 줄들.join('\n'), summary: 세말('count', ls.length), 일감수: ls.length };
911
+ return { content: 줄들.join('\n') + 버린말, summary: 세말('count', ls.length), 일감수: ls.length };
834
912
  }
835
913
 
836
914
  if (받은것.끝내기) {
@@ -854,7 +932,7 @@ export const JOBS_TOOL = {
854
932
  }
855
933
  return {
856
934
  content: `${번호}번을 끝냈습니다 (${r.초}초 돌았습니다): ${r.명령}`
857
- + (r.남은 ? `\n\n마지막 출력:\n${뒤만(r.남은, 2000).글}` : ''),
935
+ + (r.남은 ? `\n\n마지막 출력:\n${뒤만(r.남은, 2000).글}` : '') + 버린말,
858
936
  summary: 이어(말('sum.jobStopped'), 말('unit.sec', { n: r.초 })),
859
937
  };
860
938
  }
@@ -870,7 +948,7 @@ export const JOBS_TOOL = {
870
948
  ? (r.앞잘림 ? '(앞부분은 너무 길어 잘렸습니다 — 뒤쪽만 남았습니다)\n' : '') + 잘라낸.글
871
949
  : '(지난번 읽은 뒤로 새 출력이 없습니다)';
872
950
  return {
873
- content: `${머리}\n\n${몸}`,
951
+ content: `${머리}\n\n${몸}${버린말}`,
874
952
  summary: (r.상태 === '도는중' ? 이어(말('sum.jobRunning'), 말('unit.sec', { n: r.초 })) : 끝난말(r))
875
953
  + (잘라낸.줄임 ? ' · 일부만' : ''),
876
954
  // 끝난 일감이 종료코드 0 이 아니면 실패로 물들인다 — Bash 와 같은 규칙이다.