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/tools/jobs.js CHANGED
@@ -26,6 +26,11 @@
26
26
  import { spawn, execFileSync } from 'node:child_process';
27
27
  import { decode as decodeBytes, consoleCodepage } from './encoding.js';
28
28
  import { 셸명령 } from './shell.js';
29
+ import { 열쇠뺀환경 } from '../backend/mcp.js';
30
+ import { 말, 세말 } from '../i18n/index.js';
31
+
32
+ /* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
33
+ const 이어 = (...조각들) => 조각들.filter((x) => x != null && String(x) !== '').join(' · ');
29
34
 
30
35
  // 한 일감이 **들고 있을** 출력 상한. 넘으면 앞을 버리고 뒤를 남긴다 —
31
36
  // 오래 도는 것에서 필요한 건 언제나 **방금** 나온 쪽이다.
@@ -84,12 +89,87 @@ export function 띄우기옵션(cwd) {
84
89
  const win = process.platform === 'win32';
85
90
  return {
86
91
  cwd,
92
+ // 열쇠만 빼고 나머지는 그대로 물려준다 (backend/mcp.js 열쇠뺀환경 머리말).
93
+ env: 열쇠뺀환경(),
87
94
  windowsHide: true,
88
95
  detached: !win,
89
96
  stdio: ['ignore', 'pipe', 'pipe'],
90
97
  };
91
98
  }
92
99
 
100
+ /**
101
+ * 윈도우에서 pid 아래 나무를 통째로 끊는다.
102
+ *
103
+ * @returns {boolean} 나무를 **다 훑었나.** false 면 손자가 남아 있을 수 있으니,
104
+ * 부르는 쪽은 뿌리(cmd.exe)를 죽이지 말고 다시 와야 한다.
105
+ *
106
+ * ── 실패 두 가지를 갈라야 한다 ──────────────────────────────────────────
107
+ *
108
+ * · 「그런 프로세스 없다」 — 이미 죽은 것이다. 탈이 아니다.
109
+ * · **taskkill 이 제 상한에 걸려 잘렸다** — 나무를 다 못 훑었다.
110
+ *
111
+ * 뒤엣것을 앞엣것처럼 넘기고 뿌리를 죽여 버리면 손자가 영영 못 찾는 채로
112
+ * 남는다. 뿌리가 죽은 뒤에는 `taskkill /pid` 가 「프로세스를 못 찾았습니다」 로
113
+ * 끝나서 다시 부를 수조차 없다.
114
+ *
115
+ * ── 무엇으로 가르나: err.killed 가 아니다 ───────────────────────────────
116
+ *
117
+ * 처음에는 `!err?.killed` 로 갈랐다. **그게 늘 참이었다.** 비동기 execFile 의
118
+ * 콜백은 상한에 걸리면 err.killed 를 true 로 주는데, execFileSync 는 안 준다.
119
+ * 그래서 「나무를 다 못 훑었다」 가 한 번도 안 나왔고, 뿌리를 살려 두는 분기는
120
+ * 처음부터 닿지 않는 코드였다 — 이 판의 대표 기능이 통째로 헛돈 셈이다.
121
+ *
122
+ * 실제로 재 보고 갈랐다(윈도우 11 · Node 20):
123
+ *
124
+ * 상한에 걸림 killed=undefined signal=SIGTERM code=ETIMEDOUT status=null
125
+ * 없는 pid 를 죽임 killed=undefined signal=null code=undefined status=128
126
+ *
127
+ * 갈리는 자리는 **signal 과 code** 다. 아래 검사(test/jobs.test.js 의
128
+ * '★ taskkill 이 잘린 것과 이미 죽은 것을 가른다')가 이 값을 못박아 둔다.
129
+ */
130
+ export function 나무끊기(pid, 상한 = 1500) {
131
+ if (process.platform !== 'win32' || !pid) return true;
132
+ try {
133
+ execFileSync('taskkill', ['/pid', String(pid), '/t', '/f'],
134
+ { windowsHide: true, stdio: 'ignore', timeout: 상한 });
135
+ return true;
136
+ } catch (err) {
137
+ return !잘렸나(err);
138
+ }
139
+ }
140
+
141
+ /** 이 오류가 「상한에 걸려 잘렸다」 인가. 아니면 그냥 0 이 아닌 값으로 끝난 것이다. */
142
+ export function 잘렸나(err) {
143
+ return err?.code === 'ETIMEDOUT' || err?.signal != null || err?.killed === true;
144
+ }
145
+
146
+ /**
147
+ * 유닉스에서 pid 가 이끄는 **무리째** 죽인다.
148
+ *
149
+ * 음수 pid 는 '그 무리 전부' 라는 뜻이다 — 띄울 때 detached 로 무리를 만들어
150
+ * 둔 것과 짝이다. 이걸 안 하고 kid.kill() 만 하면 sh 나 npm 만 죽고 그 아래
151
+ * vite 가 포트를 문 채로 남는다. 무엇이 물고 있는지 못 찾는, 제일 나쁜 자리다.
152
+ *
153
+ * 먼저 곱게 말하고(TERM), 안 들으면 끊는다(KILL). 곧장 KILL 로 가면 dev 서버가
154
+ * 임시 파일이나 소켓을 치울 틈이 없다.
155
+ *
156
+ * **띄울 때 무리를 안 만들었으면 여기서 할 수 있는 것이 없다.** 나중에 무리를
157
+ * 만들어 줄 방법은 없다 — 그래서 이 판단이 띄우는 자리와 짝을 이뤄야 한다.
158
+ * (Bash 도구가 한동안 이 짝을 안 맞춰서, 윈도우만 고쳐지고 유닉스는 그대로였다.)
159
+ */
160
+ export function 무리끊기(pid, { 곧장 = false, 늦게 = 800 } = {}) {
161
+ if (process.platform === 'win32' || !pid) return;
162
+ if (곧장) {
163
+ try { process.kill(-pid, 'SIGKILL'); } catch { /* 이미 죽음 */ }
164
+ return;
165
+ }
166
+ try { process.kill(-pid, 'SIGTERM'); } catch { /* 무리가 없거나 이미 죽음 */ }
167
+ try {
168
+ const 시계 = setTimeout(() => { try { process.kill(-pid, 'SIGKILL'); } catch { /* 죽었다 */ } }, 늦게);
169
+ 시계.unref?.();
170
+ } catch { /* 끝나는 중이면 타이머를 못 건다 — 부르는 쪽이 곧장 으로 다시 온다 */ }
171
+ }
172
+
93
173
  /**
94
174
  * 프로세스 나무를 통째로 끝낸다. 자식만 죽이면 손자가 남아 포트를 계속 문다.
95
175
  *
@@ -106,42 +186,31 @@ export function 띄우기옵션(cwd) {
106
186
  * 그래서 동기로 부른다. 여기서 멈추는 몇십 ms 로 '안 죽은 서버' 를 없앤다.
107
187
  * test/jobs.test.js 의 '프로세스가 진짜 멈춘다' 가 이 자리를 지킨다.
108
188
  */
109
- function 나무죽이기(kid, { 파이프끊기: 끊을까 = true, 곧장 = false } = {}) {
110
- if (!kid) return;
189
+ function 나무죽이기(kid, { 파이프끊기: 끊을까 = true, 곧장 = false, 상한 = 1500, 뿌리살리기 = true } = {}) {
190
+ if (!kid) return true;
191
+ let 나무끊었나 = true;
111
192
  if (process.platform === 'win32' && kid.pid) {
112
193
  /*
113
- * 상한을 짧게 잡는다. 여기는 끝날 때도 지나가는 자리라(process 'exit'),
114
- * 일감마다 몇 초씩 기다리면 deel 이 멈춘 것처럼 보인다. taskkill 은
115
- * 실제로는 수십 ms 돌아온다 값은 그놈이 엉겼을 때의 울타리다.
194
+ * 상한은 부르는 쪽이 정한다. 여기는 끝날 때도 지나가는 자리라
195
+ * (process 'exit'), 일감마다 몇 초씩 기다리면 deel 이 멈춘 것처럼 보인다.
196
+ * 그래서 기본은 짧다. 사람이 「끝내」 라고 시킨 자리에서는 길게 준다
197
+ * 거기서는 몇백 ms 늦는 것보다 안 죽는 것이 훨씬 나쁘다.
116
198
  */
117
- try {
118
- execFileSync('taskkill', ['/pid', String(kid.pid), '/t', '/f'],
119
- { windowsHide: true, stdio: 'ignore', timeout: 1500 });
120
- } catch { /* 이미 죽었으면 0 이 아닌 값으로 끝난다 — 그건 탈이 아니다 */ }
121
- } else if (kid.pid) {
199
+ 나무끊었나 = 나무끊기(kid.pid, 상한);
122
200
  /*
123
- * 유닉스는 **무리째** 죽인다.
201
+ * 나무를 못 훑었으면 뿌리를 살려 둔다. 부르는 쪽이 다시 온다.
124
202
  *
125
- * 음수 pid '그 무리 전부' 라는 뜻이다(띄우기옵션 detached 짝).
126
- * 이걸 안 하고 kid.kill() 하면 sh npm 죽고 그 아래 vite 가
127
- * 포트를 문 채로 남는다 — 무엇이 물고 있는지 못 찾는, 제일 나쁜 자리다.
128
- *
129
- * 먼저 곱게 말하고(TERM), 안 들으면 끊는다(KILL). 곧장 KILL 로 가면
130
- * dev 서버가 임시 파일이나 소켓을 치울 틈이 없다.
203
+ * 자리만 예외다 이상 다시 없는 자리(프로그램이 끝나는
204
+ * )에서는 뿌리라도 죽인다. 살려 두는 것보다는 낫다.
131
205
  */
132
- if (곧장) {
133
- try { process.kill(-kid.pid, 'SIGKILL'); } catch { /* 이미 죽음 */ }
134
- } else {
135
- try { process.kill(-kid.pid, 'SIGTERM'); } catch { /* 무리가 없거나 이미 죽음 */ }
136
- try {
137
- const 시계 = setTimeout(() => { try { process.kill(-kid.pid, 'SIGKILL'); } catch { /* 죽었다 */ } }, 800);
138
- 시계.unref?.();
139
- } catch { /* 끝나는 중이면 타이머를 못 건다 — 부르는 쪽이 곧장 으로 다시 온다 */ }
140
- }
206
+ if (!나무끊었나 && 뿌리살리기) return false;
207
+ } else if (kid.pid) {
208
+ 무리끊기(kid.pid, { 곧장 });
141
209
  }
142
210
  try { kid.kill(); } catch { /* 이미 죽음 */ }
143
211
  if (끊을까) 파이프끊기(kid);
144
212
  try { kid.unref(); } catch { /* 없으면 그만 */ }
213
+ return 나무끊었나;
145
214
  }
146
215
 
147
216
  /**
@@ -435,7 +504,15 @@ export async function 끝내기(번호) {
435
504
  if (!j) return null;
436
505
  if (j.상태 !== '도는중') { 일감들.delete(j.번호); return { 이미: true, 명령: j.명령 }; }
437
506
  j.상태 = '죽임';
438
- 나무죽이기(j.kid, { 파이프끊기: false });
507
+ /*
508
+ * 사람이 시킨 자리라 넉넉히 기다린다.
509
+ *
510
+ * 검사를 통째로 돌릴 때처럼 컴퓨터가 바쁘면 taskkill 이 1.5초를 넘긴다.
511
+ * 그때 잘린 채로 넘어가면 손자가 남는데, 몇백 ms 빨리 돌아오는 것보다
512
+ * 안 죽은 서버 하나가 훨씬 비싸다.
513
+ */
514
+ const 넉넉히 = 6000;
515
+ 나무죽이기(j.kid, { 파이프끊기: false, 상한: 넉넉히 });
439
516
  await 죽는말기다리기(j.kid, 죽는말기다림);
440
517
  /*
441
518
  * 아직 살아 있으면 여기서 끊는다.
@@ -443,10 +520,44 @@ export async function 끝내기(번호) {
443
520
  * 곧 목록에서 지울 참인데, 지우고 나면 이놈을 가리킬 방법이 없다. 안 죽은
444
521
  * 채로 지우면 영영 못 찾는 프로세스가 하나 남는다 — 이 기능이 없애려던
445
522
  * 바로 그 상태다. 유닉스에서 SIGTERM 을 안 듣는 놈이 여기로 온다.
523
+ * 윈도우에서는 나무를 다 못 훑어 뿌리를 살려 둔 경우가 여기로 온다.
524
+ *
525
+ * **뿌리살리기를 끈다.** 이걸 안 끄면 윈도우에서 taskkill 이 또 잘렸을 때
526
+ * 나무죽이기 가 kid.kill() 에 닿지도 못하고 그냥 돌아온다 — 「곧장」 이
527
+ * 윈도우에서는 아무 뜻이 없었다는 말이다. 여기는 마지막 자리라 뿌리라도
528
+ * 죽여야 한다. (모두끝내기 는 이미 그렇게 하고 있었다.)
446
529
  */
447
- if (아직사나(j.kid)) 나무죽이기(j.kid, { 파이프끊기: false, 곧장: true });
530
+ if (아직사나(j.kid)) {
531
+ 나무죽이기(j.kid, { 파이프끊기: false, 곧장: true, 상한: 넉넉히, 뿌리살리기: false });
532
+ /*
533
+ * 죽인 **직후에 살았나 물으면 안 된다.**
534
+ *
535
+ * 아직사나() 는 exitCode·signalCode 를 보는데, 그 값은 Node 가 종료를
536
+ * 실제로 **관찰한 뒤에** 채워진다. 동기 taskkill 이 돌아온 순간에는 아직
537
+ * 비어 있을 수 있다 — 그러면 멀쩡히 죽은 것을 「안 죽었다」 로 읽는다.
538
+ *
539
+ * 검사를 통째로 돌릴 때 이걸로 한 번 헛짚었다. 혼자 돌리면 초록이고
540
+ * 전체에서만 빨개졌다 — 컴퓨터가 바쁘면 관찰이 늦기 때문이다.
541
+ * 그러니 한 번 더 기다렸다가 묻는다.
542
+ */
543
+ await 죽는말기다리기(j.kid, 죽는말기다림);
544
+ }
448
545
  파이프끊기(j.kid);
449
546
  const 남은 = j.읽기({});
547
+ /*
548
+ * ★ 그러고도 안 죽었으면 **그렇다고 말한다.**
549
+ *
550
+ * 전에는 성패를 안 보고 늘 성공을 돌려줬다. 그러면 모델과 사람은 「끝냈습니다」
551
+ * 를 받는데 서버는 살아서 포트를 물고 있고, 목록에서도 지워져 다시 부를
552
+ * 방법이 없다. 이 파일 머리말이 스스로 적어 둔 「안 죽은 채로 지우면 영영
553
+ * 못 찾는 프로세스가 하나 남는다」 가 그대로 성립하고 있었다.
554
+ *
555
+ * 안 죽었으면 목록에 남긴다. 그래야 사람이 한 번 더 시킬 수 있다.
556
+ */
557
+ if (아직사나(j.kid)) {
558
+ j.상태 = '도는중'; // 지우지 않는다 — 다시 시킬 수 있어야 한다
559
+ return { 이미: false, 안죽음: true, 명령: j.명령, 초: j.산햇수(), 남은: 남은.글 };
560
+ }
450
561
  일감들.delete(j.번호);
451
562
  return { 이미: false, 명령: j.명령, 초: j.산햇수(), 남은: 남은.글 };
452
563
  }
@@ -460,7 +571,14 @@ export async function 끝내기(번호) {
460
571
  export function 모두끝내기() {
461
572
  const n = [...일감들.values()].filter((j) => j.상태 === '도는중').length;
462
573
  for (const j of 일감들.values()) {
463
- if (j.상태 === '도는중') { j.상태 = '죽임'; 나무죽이기(j.kid); }
574
+ if (j.상태 !== '도는중') continue;
575
+ j.상태 = '죽임';
576
+ /*
577
+ * 여기는 프로그램이 끝나는 길이라 **다시 올 기회가 없다.** 한 번 짧게
578
+ * 훑어 보고, 잘렸으면 한 번 더 길게 훑는다. 그러고도 안 되면 뿌리라도
579
+ * 죽인다 — 아무것도 안 죽이는 것보다 낫다.
580
+ */
581
+ if (!나무죽이기(j.kid)) 나무죽이기(j.kid, { 상한: 4000, 뿌리살리기: false });
464
582
  }
465
583
  일감들.clear();
466
584
  return n;
@@ -500,9 +618,9 @@ process.once('exit', () => { try { 모두끝내기(); } catch { /* 끝나는 중
500
618
  * 두 벌이 되면 한쪽만 고쳐지고, 그때부터 화면과 실제가 어긋난다.
501
619
  */
502
620
  const 인자이름 = {
503
- 번호: ['번호', 'job', 'job_id', 'jobId', 'id', 'number'],
504
- 끝내기: ['끝내기', 'stop', 'kill'],
505
- 처음부터: ['처음부터', 'from_start', 'fromStart', 'full'],
621
+ 번호: ['job', 'job_id', 'jobId', 'id', 'number', '번호'],
622
+ 끝내기: ['stop', 'kill', '끝내기'],
623
+ 처음부터: ['from_start', 'fromStart', 'full', '처음부터'],
506
624
  };
507
625
  const 아는이름 = new Set(Object.values(인자이름).flat());
508
626
 
@@ -555,14 +673,14 @@ export const JOBS_TOOL = {
555
673
  schema: {
556
674
  name: 'Jobs',
557
675
  description: '뒤에서 도는 명령(Bash 의 background)을 보고·읽고·끝낸다.'
558
- + ' 번호 없이 부르면 목록. 번호를 주면 그동안 새로 나온 출력.'
559
- + ' 서버를 띄웠으면 일이 끝날 때 반드시 끝내기로 정리해라.',
676
+ + ' job 없이 부르면 목록. job 주면 그동안 새로 나온 출력.'
677
+ + ' 서버를 띄웠으면 일이 끝날 때 반드시 stop 으로 정리해라.',
560
678
  parameters: {
561
679
  type: 'object',
562
680
  properties: {
563
- 번호: { type: 'number', description: '볼 일감 번호 (job). 없으면 목록' },
564
- 끝내기: { type: 'boolean', description: 'true 면 그 일감을 끝낸다 (stop)' },
565
- 처음부터: { type: 'boolean', description: 'true 면 처음부터 다시 읽는다 (from_start)' },
681
+ job: { type: 'number', description: '볼 일감 번호. 없으면 목록' },
682
+ stop: { type: 'boolean', description: 'true 면 그 일감을 끝낸다' },
683
+ from_start: { type: 'boolean', description: 'true 면 처음부터 다시 읽는다' },
566
684
  },
567
685
  required: [],
568
686
  },
@@ -612,7 +730,7 @@ export const JOBS_TOOL = {
612
730
  };
613
731
  }
614
732
  const ls = 목록();
615
- if (!ls.length) return { content: '뒤에서 도는 명령이 없습니다.', summary: '0개' };
733
+ if (!ls.length) return { content: '뒤에서 도는 명령이 없습니다.', summary: 세말('count', 0) };
616
734
  const 줄들 = ls.map((j) => {
617
735
  const 상 = j.상태 === '도는중' ? `도는중 ${j.초}초` : j.시그널 ? `${j.시그널} 로 죽음` : `끝남 (종료코드 ${j.종료코드})`;
618
736
  const 새것 = j.안읽은글자 ? ` · 안 읽은 출력 ${j.안읽은글자}자` : '';
@@ -626,17 +744,32 @@ export const JOBS_TOOL = {
626
744
  * 그러면 포트가 물려서 안 뜨고, 왜 안 되는지도 모른다.
627
745
  */
628
746
  if (치운것) 줄들.push(` (오래된 것 ${치운것}개는 자리를 위해 지웠습니다)`);
629
- return { content: 줄들.join('\n'), summary: `${ls.length}개`, 일감수: ls.length };
747
+ return { content: 줄들.join('\n'), summary: 세말('count', ls.length), 일감수: ls.length };
630
748
  }
631
749
 
632
750
  if (받은것.끝내기) {
633
751
  const r = await 끝내기(번호);
634
752
  if (!r) return { error: `${번호}번 일감이 없습니다. 번호 없이 Jobs 를 불러 목록을 보세요.` };
635
- if (r.이미) return { content: `${번호}번은 이미 끝나 있었습니다: ${r.명령}`, summary: '이미 끝남' };
753
+ if (r.이미) return { content: `${번호}번은 이미 끝나 있었습니다: ${r.명령}`, summary: 말('sum.jobEndedAlready') };
754
+ /*
755
+ * ★ 안 죽었으면 「끝냈습니다」 라고 하지 않는다.
756
+ *
757
+ * 여기서 거짓말하면 모델은 다음 단계로 가고 사람은 포트를 문 채로 남은
758
+ * 서버를 못 찾는다 — 이 기능이 없애려던 바로 그 상태다. 목록에도
759
+ * 남겨 뒀으니 한 번 더 시킬 수 있다고 알려 준다.
760
+ */
761
+ if (r.안죽음) {
762
+ return {
763
+ error: `${번호}번이 안 끝났습니다 (${r.초}초 돌았습니다): ${r.명령}`
764
+ + '\n죽이라고 시켰는데 아직 살아 있습니다. 목록에 그대로 두었으니 한 번 더 시켜 보세요.'
765
+ + (r.남은 ? `\n\n마지막 출력:\n${뒤만(r.남은, 2000).글}` : ''),
766
+ summary: 말('sum.jobNotStopped'),
767
+ };
768
+ }
636
769
  return {
637
770
  content: `${번호}번을 끝냈습니다 (${r.초}초 돌았습니다): ${r.명령}`
638
771
  + (r.남은 ? `\n\n마지막 출력:\n${뒤만(r.남은, 2000).글}` : ''),
639
- summary: `끝냄 · ${r.초}초`,
772
+ summary: 이어(말('sum.jobStopped'), 말('unit.sec', { n: r.초 })),
640
773
  };
641
774
  }
642
775
 
@@ -652,7 +785,7 @@ export const JOBS_TOOL = {
652
785
  : '(지난번 읽은 뒤로 새 출력이 없습니다)';
653
786
  return {
654
787
  content: `${머리}\n\n${몸}`,
655
- summary: (r.상태 === '도는중' ? `도는중 · ${r.초}초` : 끝난말(r))
788
+ summary: (r.상태 === '도는중' ? 이어(말('sum.jobRunning'), 말('unit.sec', { n: r.초 })) : 끝난말(r))
656
789
  + (잘라낸.줄임 ? ' · 일부만' : ''),
657
790
  // 끝난 일감이 종료코드 0 이 아니면 실패로 물들인다 — Bash 와 같은 규칙이다.
658
791
  // 시그널로 죽은 것도 실패다 — 그때 종료코드는 null 이라 !== 0 으로 잡힌다.
package/src/tools/lsp.js CHANGED
@@ -27,6 +27,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
27
27
  import { 얻기, 색인중일까 } from '../lsp/client.js';
28
28
  import { 갈래, 프로젝트갈래 } from '../lsp/servers.js';
29
29
  import { 찾을개수 } from '../agent/budget.js';
30
+ import { 말, 세말 } from '../i18n/index.js';
30
31
 
31
32
  /** 한 자리를 사람이 읽을 한 줄로. 그 줄의 글까지 붙여야 열어 보지 않고도 안다. */
32
33
  function 한줄(scope, uri, 범위) {
@@ -240,11 +241,11 @@ export const DEF_TOOL = {
240
241
  곳들 = 후보.map((c) => 한줄(ctx.scope, c.uri, c.range));
241
242
  어디서 = 'workspace/symbol';
242
243
  }
243
- if (!곳들.length) return { summary: `${이름}: 정의를 못 찾았습니다. Grep 으로 찾아보세요.`, found: 0 };
244
+ if (!곳들.length) return { summary: `${이름}: ${말('lsp.noDef')}`, found: 0 };
244
245
 
245
246
  const 여럿 = 여럿이면(후보, ctx.scope);
246
247
  return {
247
- summary: `${이름} — 정의 ${곳들.length}곳${여럿 ? `\n${여럿}` : ''}`,
248
+ summary: `${이름} — ${말('lsp.defs', { n: 세말('places', 곳들.length) })}${여럿 ? `\n${여럿}` : ''}`,
248
249
  found: 곳들.length,
249
250
  source: 어디서,
250
251
  locations: 곳들,
@@ -288,7 +289,7 @@ export const REFS_TOOL = {
288
289
  const 곳들 = 자리들펴기(답.값).map((x) => 한줄(ctx.scope, x.uri, x.range));
289
290
  if (!곳들.length) {
290
291
  return {
291
- summary: `${이름}: 쓰는 자리가 없습니다.`
292
+ summary: `${이름}: ${말('lsp.noRefs')}`
292
293
  + ' 정말 안 쓰는 것일 수도 있고, 언어 서버가 아직 색인 중일 수도 있습니다 —'
293
294
  + ' 지우기 전에 Grep 으로 한 번 더 보세요.',
294
295
  found: 0,
@@ -314,7 +315,7 @@ export const REFS_TOOL = {
314
315
 
315
316
  const 여럿 = 여럿이면(후보, ctx.scope);
316
317
  return {
317
- summary: `${이름} — 쓰는 자리 ${곳들.length}곳 · 파일 ${묶음.size}개`
318
+ summary: `${이름} — ${말('lsp.refs', { 자리: 세말('places', 곳들.length), 파일: 세말('files', 묶음.size) })}`
318
319
  + (남은 ? ` (${남은}곳은 자리가 모자라 안 실었습니다)` : '')
319
320
  + (여럿 ? `\n${여럿}` : ''),
320
321
  found: 곳들.length,
@@ -35,6 +35,10 @@ import { walk, SKIP_DIRS, globToRegex, 내부살림 } from './fsutil.js';
35
35
  import { 건너뜀말 } from './ignore.js';
36
36
  import { decode, looksBinary } from './encoding.js';
37
37
  import { 찾을개수, 뼈대줄수 } from '../agent/budget.js';
38
+ import { 말, 세말 } from '../i18n/index.js';
39
+
40
+ /* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
41
+ const 이어 = (...조각들) => 조각들.filter((x) => x != null && String(x) !== '').join(' · ');
38
42
 
39
43
  /**
40
44
  * 언어별로 '뼈대' 를 이루는 것들.
@@ -243,7 +247,7 @@ export const OUTLINE_TOOL = {
243
247
  파일들 = 파일들.filter((f) => re.test(f.rel) || re.test(f.rel.split('/').pop()));
244
248
  }
245
249
 
246
- if (!파일들.length) return { content: '볼 파일이 없습니다.', summary: '0개' };
250
+ if (!파일들.length) return { content: '볼 파일이 없습니다.', summary: 세말('count', 0) };
247
251
 
248
252
  // 최근에 손댄 것부터. 지금 하는 일과 가까울 가능성이 높다.
249
253
  파일들.sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0));
@@ -327,8 +331,8 @@ export const OUTLINE_TOOL = {
327
331
 
328
332
  return {
329
333
  content: 줄들.join('\n').trimEnd() + 건너뜀,
330
- summary: `${뼈대있는파일}개 파일 · ${항목수}곳`
331
- + (못읽은것.size ? ` · 읽음 ${[...못읽은것.values()].reduce((a, x) => a + x.length, 0)}개` : ''),
334
+ summary: 이어(세말('files', 뼈대있는파일), 세말('places', 항목수))
335
+ + (못읽은것.size ? ` · ${말('sum.unread', { n: [...못읽은것.values()].reduce((a, x) => a + x.length, 0) })}` : ''),
332
336
  };
333
337
  },
334
338
  };
package/src/tools/task.js CHANGED
@@ -43,19 +43,19 @@ export const TASK_TOOL = {
43
43
  + ' 처음부터 끝까지 일하고, 너에게는 결과 요약만 돌아온다 — 하위가 읽은 파일 내용은'
44
44
  + ' 네 창에 안 쌓인다. 그래서 파일 여러 개를 만들거나 고치는 일은 이걸로 나눠야 끝까지 간다.'
45
45
  + ' 덩이 하나는 혼자 끝낼 수 있는 크기여야 한다 (예: "index.html 과 style.css 만들기").'
46
- + ' 하위는 네 대화를 못 본다 — 필요한 것은 할일 안에 다 적어 줘라.'
46
+ + ' 하위는 네 대화를 못 본다 — 필요한 것은 task 안에 다 적어 줘라.'
47
47
  + ' 짧은 일 하나를 할 때는 쓰지 마라. 그냥 직접 하는 편이 빠르다.',
48
48
  parameters: {
49
49
  type: 'object',
50
50
  properties: {
51
- 목적: { type: 'string', description: '이 덩이를 한 줄로 (예: "대시보드 화면 뼈대 만들기")' },
52
- 할일: {
51
+ purpose: { type: 'string', description: '이 덩이를 한 줄로 (예: "대시보드 화면 뼈대 만들기")' },
52
+ task: {
53
53
  type: 'string',
54
54
  description:
55
55
  '하위가 할 일을 빠짐없이. 하위는 지금 대화를 못 보니 필요한 배경·정한 것·파일 경로를'
56
56
  + ' 여기 다 적어라. 무엇이 끝나면 다 된 것인지도 적어라.',
57
57
  },
58
- 모드: {
58
+ mode: {
59
59
  type: 'string',
60
60
  description: '하위가 일할 방식: code(만들고 고침) · debug(원인 찾기) · ask(읽고 답만). 안 적으면 code.',
61
61
  },
@@ -72,7 +72,7 @@ export const TASK_TOOL = {
72
72
  * 약속을 그 자리에서 깨는 것이다. 실제 판단은 loop.js 가 한다 —
73
73
  * 부탁으로 막지 않는다.
74
74
  */
75
- 모델: {
75
+ model: {
76
76
  type: 'string',
77
77
  description:
78
78
  '이 덩이를 **다른 모델**에게 맡긴다. 사람이 설정에 적어 둔 프로필 이름만 쓸 수 있다'
@@ -81,7 +81,11 @@ export const TASK_TOOL = {
81
81
  + ' 판단이 필요한 일은 네가 직접 해라.',
82
82
  },
83
83
  },
84
- required: ['목적', '할일'],
84
+ // properties 와 **같은 이름**이어야 한다. 이름을 바꾸면서 여기만
85
+ // 옛 이름으로 남겨 두면, 서버는 "없는 인자를 필수라고 한다" 고 거절하거나
86
+ // (더 나쁘게) 그냥 받아 놓고 모델이 아무것도 안 채우게 둔다.
87
+ // test/toolargs.test.js 가 이 짝을 맞춰 본다.
88
+ required: ['purpose', 'task'],
85
89
  },
86
90
  },
87
91
 
package/src/tools/todo.js CHANGED
@@ -9,6 +9,10 @@
9
9
  // 여러 개를 한꺼번에 '하는 중' 으로 두면 결국 아무것도 안 끝난다.
10
10
  // 여기서 막아 두면 모델이 순서를 정하고 하나씩 닫는다.
11
11
 
12
+ import { 말 } from '../i18n/index.js';
13
+
14
+ /* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
15
+ const 이어 = (...조각들) => 조각들.filter((x) => x != null && String(x) !== '').join(' · ');
12
16
  export const STATES = ['todo', 'doing', 'done'];
13
17
 
14
18
  const 표시 = { todo: '☐', doing: '▶', done: '☑' };
@@ -117,7 +121,7 @@ export const TODO_TOOL = {
117
121
  + '이 목록은 앞에 보낸 것과 글자 하나까지 같습니다 — 바뀐 것이 없습니다.\n'
118
122
  + '방금 끝낸 일이 있으면 그 줄을 done 으로 바꿔서 보내고,'
119
123
  + ' 없으면 목록을 다시 보내지 말고 다음 일을 하세요.',
120
- summary: '그대로 (바뀐 것 없음)',
124
+ summary: 말('sum.noChange'),
121
125
  todos: items,
122
126
  안바뀜: true,
123
127
  };
@@ -129,7 +133,10 @@ export const TODO_TOOL = {
129
133
 
130
134
  return {
131
135
  content: render(items),
132
- summary: `${끝난것}/${items.length} 완료${새로끝난.length ? ` · 방금 ${새로끝난.length}개` : ''}`,
136
+ summary: 이어(
137
+ 말('sum.doneOf', { 끝: 끝난것, 전체: items.length }),
138
+ 새로끝난.length ? 말('sum.justDone', { n: 새로끝난.length }) : '',
139
+ ),
133
140
  todos: items,
134
141
  };
135
142
  },
@@ -34,6 +34,10 @@ import { walk, SKIP_DIRS, 내부살림 } from './fsutil.js';
34
34
  import { 건너뜀말 } from './ignore.js';
35
35
  import { decode, looksBinary } from './encoding.js';
36
36
  import { checkCommand } from '../safety/guard.js';
37
+ import { 말 } from '../i18n/index.js';
38
+
39
+ /* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
40
+ const 이어 = (...조각들) => 조각들.filter((x) => x != null && String(x) !== '').join(' · ');
37
41
 
38
42
  /** 짝이 맞아야 하는 HTML 태그. 안 닫아도 되는 것(void)은 뺀다. */
39
43
  const 안닫아도되는것 = new Set([
@@ -118,6 +122,15 @@ function 명령돌리기(cmd, cwd, 제한 = 60000) {
118
122
  });
119
123
  }
120
124
 
125
+ /**
126
+ * 폴더를 통째로 볼 때 한 번에 보는 파일 수.
127
+ *
128
+ * 상한이 있는 것 자체는 옳다 — 저장소가 크면 한 번의 Verify 가 몇 분이 된다.
129
+ * 다만 **넘친 것을 말해야** 한다. 안 말하면 마흔한 번째 파일이 깨져 있어도
130
+ * 「확인했습니다」 로 끝난다. 이 파일이 있는 까닭과 정면으로 어긋난다.
131
+ */
132
+ const 한번에최대 = 40;
133
+
121
134
  /** 이 확장자를 돌려 볼 명령이 있나. 없으면 null — 그러면 '못 돌려 봤다' 고 말한다. */
122
135
  function 돌릴명령(확장) {
123
136
  switch (확장) {
@@ -161,6 +174,8 @@ export const VERIFY_TOOL = {
161
174
 
162
175
  // 볼 파일 고르기. 안 주면 작업 폴더에서 확인할 수 있는 것을 찾는다.
163
176
  let 볼것 = [];
177
+ // 상한에 걸려 안 본 것의 수. 0 이 아니면 아래에서 못한것 에 넣는다.
178
+ let 넘친수 = 0;
164
179
  // .gitignore 로 안 본 것은 셈해 뒀다가 끝에 적는다. 조용히 빼면
165
180
  // "빌드 산출물에 탈이 있는데 왜 확인이 통과냐" 를 사람이 못 푼다.
166
181
  const 건너뜀 = { 폴더: 0, 파일: 0 };
@@ -186,15 +201,33 @@ export const VERIFY_TOOL = {
186
201
  } else {
187
202
  const 전부 = walk(뿌리, { skipDirs: SKIP_DIRS });
188
203
  셈더하기(전부);
189
- 볼것 = 전부
190
- .filter((f) => ['.html', '.htm', '.css', '.json', '.js', '.mjs', '.cjs', '.py'].includes(extname(f.path).toLowerCase()))
191
- .slice(0, 40)
192
- .map((f) => ({ path: f.path }));
204
+ const 볼만한것 = 전부
205
+ .filter((f) => ['.html', '.htm', '.css', '.json', '.js', '.mjs', '.cjs', '.py'].includes(extname(f.path).toLowerCase()));
206
+ 볼것 = 볼만한것.slice(0, 한번에최대).map((f) => ({ path: f.path }));
207
+ /*
208
+ * 상한에 걸려 안 본 것은 **말한다.**
209
+ *
210
+ * 이 파일 머리말이 스스로 못 박아 둔 것이 있다 — 「못 확인한 것은 못
211
+ * 확인했다고 말한다. 확인 못 한 것을 확인했다고 하는 것이 제일 나쁘다.」
212
+ * .gitignore 로 건너뛴 것과 walk 상한은 그렇게 하고 있었는데, **이
213
+ * 상한만 아무 데도 안 적혔다.**
214
+ *
215
+ * 그래서 파일이 마흔 개를 넘으면, 마흔한 번째부터는 깨져 있어도
216
+ * 「확인했습니다」 로 끝났다. failed 도 거짓이라 루프까지 성공으로
217
+ * 넘어간다. 상한 자체보다 **말을 안 한 것**이 탈이다.
218
+ */
219
+ 넘친수 = 볼만한것.length - 볼것.length;
193
220
  }
194
221
 
195
222
  const 된것 = [];
196
223
  const 탈난것 = [];
197
224
  const 못한것 = [];
225
+ if (넘친수 > 0) {
226
+ 못한것.push({
227
+ 이름: `그 밖의 ${넘친수}개`,
228
+ 왜: `한 번에 ${한번에최대}개까지만 봅니다 — 나머지는 paths 로 짚어서 다시 부르세요`,
229
+ });
230
+ }
198
231
 
199
232
  for (const x of 볼것) {
200
233
  if (x.없음) { 탈난것.push({ 이름: x.없음, 탈: [x.왜] }); continue; }
@@ -290,9 +323,11 @@ export const VERIFY_TOOL = {
290
323
  const 다됐나 = !탈난것.length;
291
324
  return {
292
325
  content: 줄.join('\n').trim() + 건너뜀말(건너뜀, 잘림, 상한),
293
- summary: 탈난것.length
294
- ? `탈 ${탈난것.length} · 확인 ${된것.length}개`
295
- : `확인 ${된것.length}개` + (못한것.length ? ` · 못 확인 ${못한것.length}개` : ''),
326
+ summary: 이어(
327
+ 탈난것.length ? 말('sum.broken', { n: 탈난것.length }) : '',
328
+ 말('sum.verified', { n: 된것.length }),
329
+ !탈난것.length && 못한것.length ? 말('sum.unverified', { n: 못한것.length }) : '',
330
+ ),
296
331
  // 루프가 '아직 안 끝났다' 고 알 수 있게. 탈이 났는데 성공으로 넘기면
297
332
  // 다음 걸음에서 모델이 "다 됐습니다" 로 답을 맺는다.
298
333
  failed: !다됐나,