deel-local-cli 1.8.0 → 1.12.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 +10 -25
  2. package/README.md +43 -51
  3. package/bin/deel.js +56 -13
  4. package/package.json +60 -60
  5. package/src/acp/serve.js +810 -732
  6. package/src/agent/budget.js +156 -153
  7. package/src/agent/compact.js +420 -296
  8. package/src/agent/effort.js +308 -177
  9. package/src/agent/evidence.js +2 -0
  10. package/src/agent/evolve.js +263 -213
  11. package/src/agent/filemem.js +155 -0
  12. package/src/agent/grade.js +51 -1
  13. package/src/agent/loop.js +1707 -1349
  14. package/src/agent/memory.js +156 -152
  15. package/src/agent/mention.js +210 -210
  16. package/src/agent/session.js +1049 -832
  17. package/src/agent/store.js +186 -9
  18. package/src/agent/threads.js +26 -1
  19. package/src/backend/adapter.js +1159 -615
  20. package/src/backend/cachemark.js +150 -0
  21. package/src/backend/detect.js +327 -288
  22. package/src/backend/learn.js +24 -0
  23. package/src/backend/mcp.js +96 -5
  24. package/src/backend/probe.js +131 -61
  25. package/src/backend/quota.js +250 -133
  26. package/src/backend/retry.js +30 -6
  27. package/src/backend/tokens.js +37 -0
  28. package/src/backend/toolfit.js +352 -0
  29. package/src/backend/wire.js +600 -0
  30. package/src/commands.js +3135 -2918
  31. package/src/config.js +47 -2
  32. package/src/i18n/en.js +544 -466
  33. package/src/i18n/index.js +18 -0
  34. package/src/i18n/ja.js +497 -417
  35. package/src/i18n/ko.js +591 -466
  36. package/src/i18n/zh.js +497 -417
  37. package/src/lsp/client.js +49 -5
  38. package/src/oneshot.js +620 -471
  39. package/src/pack/sbom.js +30 -4
  40. package/src/pack/selfpack.js +25 -9
  41. package/src/pack/sheet.en.js +288 -0
  42. package/src/pack/tar.js +65 -2
  43. package/src/plugins/manage.js +46 -9
  44. package/src/providers/bedrock.js +17 -0
  45. package/src/repl.js +2602 -2286
  46. package/src/safety/audit.js +92 -8
  47. package/src/safety/authcmd.js +14 -3
  48. package/src/safety/guard.js +143 -0
  49. package/src/safety/keystore.js +62 -39
  50. package/src/safety/undo.js +24 -6
  51. package/src/tools/fsutil.js +265 -250
  52. package/src/tools/index.js +239 -34
  53. package/src/tools/jobs.js +158 -29
  54. package/src/tools/verify.js +358 -328
  55. package/src/tools/webfetch.js +92 -9
  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/src/ui/status.js +613 -610
package/src/tools/jobs.js CHANGED
@@ -26,6 +26,7 @@
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';
29
30
  import { 말, 세말 } from '../i18n/index.js';
30
31
 
31
32
  /* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
@@ -88,12 +89,87 @@ export function 띄우기옵션(cwd) {
88
89
  const win = process.platform === 'win32';
89
90
  return {
90
91
  cwd,
92
+ // 열쇠만 빼고 나머지는 그대로 물려준다 (backend/mcp.js 열쇠뺀환경 머리말).
93
+ env: 열쇠뺀환경(),
91
94
  windowsHide: true,
92
95
  detached: !win,
93
96
  stdio: ['ignore', 'pipe', 'pipe'],
94
97
  };
95
98
  }
96
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
+
97
173
  /**
98
174
  * 프로세스 나무를 통째로 끝낸다. 자식만 죽이면 손자가 남아 포트를 계속 문다.
99
175
  *
@@ -110,42 +186,31 @@ export function 띄우기옵션(cwd) {
110
186
  * 그래서 동기로 부른다. 여기서 멈추는 몇십 ms 로 '안 죽은 서버' 를 없앤다.
111
187
  * test/jobs.test.js 의 '프로세스가 진짜 멈춘다' 가 이 자리를 지킨다.
112
188
  */
113
- function 나무죽이기(kid, { 파이프끊기: 끊을까 = true, 곧장 = false } = {}) {
114
- if (!kid) return;
189
+ function 나무죽이기(kid, { 파이프끊기: 끊을까 = true, 곧장 = false, 상한 = 1500, 뿌리살리기 = true } = {}) {
190
+ if (!kid) return true;
191
+ let 나무끊었나 = true;
115
192
  if (process.platform === 'win32' && kid.pid) {
116
193
  /*
117
- * 상한을 짧게 잡는다. 여기는 끝날 때도 지나가는 자리라(process 'exit'),
118
- * 일감마다 몇 초씩 기다리면 deel 이 멈춘 것처럼 보인다. taskkill 은
119
- * 실제로는 수십 ms 돌아온다 값은 그놈이 엉겼을 때의 울타리다.
194
+ * 상한은 부르는 쪽이 정한다. 여기는 끝날 때도 지나가는 자리라
195
+ * (process 'exit'), 일감마다 몇 초씩 기다리면 deel 이 멈춘 것처럼 보인다.
196
+ * 그래서 기본은 짧다. 사람이 「끝내」 라고 시킨 자리에서는 길게 준다
197
+ * 거기서는 몇백 ms 늦는 것보다 안 죽는 것이 훨씬 나쁘다.
120
198
  */
121
- try {
122
- execFileSync('taskkill', ['/pid', String(kid.pid), '/t', '/f'],
123
- { windowsHide: true, stdio: 'ignore', timeout: 1500 });
124
- } catch { /* 이미 죽었으면 0 이 아닌 값으로 끝난다 — 그건 탈이 아니다 */ }
125
- } else if (kid.pid) {
199
+ 나무끊었나 = 나무끊기(kid.pid, 상한);
126
200
  /*
127
- * 유닉스는 **무리째** 죽인다.
201
+ * 나무를 못 훑었으면 뿌리를 살려 둔다. 부르는 쪽이 다시 온다.
128
202
  *
129
- * 음수 pid '그 무리 전부' 라는 뜻이다(띄우기옵션 detached 짝).
130
- * 이걸 안 하고 kid.kill() 하면 sh npm 죽고 그 아래 vite 가
131
- * 포트를 문 채로 남는다 — 무엇이 물고 있는지 못 찾는, 제일 나쁜 자리다.
132
- *
133
- * 먼저 곱게 말하고(TERM), 안 들으면 끊는다(KILL). 곧장 KILL 로 가면
134
- * dev 서버가 임시 파일이나 소켓을 치울 틈이 없다.
203
+ * 자리만 예외다 이상 다시 없는 자리(프로그램이 끝나는
204
+ * )에서는 뿌리라도 죽인다. 살려 두는 것보다는 낫다.
135
205
  */
136
- if (곧장) {
137
- try { process.kill(-kid.pid, 'SIGKILL'); } catch { /* 이미 죽음 */ }
138
- } else {
139
- try { process.kill(-kid.pid, 'SIGTERM'); } catch { /* 무리가 없거나 이미 죽음 */ }
140
- try {
141
- const 시계 = setTimeout(() => { try { process.kill(-kid.pid, 'SIGKILL'); } catch { /* 죽었다 */ } }, 800);
142
- 시계.unref?.();
143
- } catch { /* 끝나는 중이면 타이머를 못 건다 — 부르는 쪽이 곧장 으로 다시 온다 */ }
144
- }
206
+ if (!나무끊었나 && 뿌리살리기) return false;
207
+ } else if (kid.pid) {
208
+ 무리끊기(kid.pid, { 곧장 });
145
209
  }
146
210
  try { kid.kill(); } catch { /* 이미 죽음 */ }
147
211
  if (끊을까) 파이프끊기(kid);
148
212
  try { kid.unref(); } catch { /* 없으면 그만 */ }
213
+ return 나무끊었나;
149
214
  }
150
215
 
151
216
  /**
@@ -439,7 +504,15 @@ export async function 끝내기(번호) {
439
504
  if (!j) return null;
440
505
  if (j.상태 !== '도는중') { 일감들.delete(j.번호); return { 이미: true, 명령: j.명령 }; }
441
506
  j.상태 = '죽임';
442
- 나무죽이기(j.kid, { 파이프끊기: false });
507
+ /*
508
+ * 사람이 시킨 자리라 넉넉히 기다린다.
509
+ *
510
+ * 검사를 통째로 돌릴 때처럼 컴퓨터가 바쁘면 taskkill 이 1.5초를 넘긴다.
511
+ * 그때 잘린 채로 넘어가면 손자가 남는데, 몇백 ms 빨리 돌아오는 것보다
512
+ * 안 죽은 서버 하나가 훨씬 비싸다.
513
+ */
514
+ const 넉넉히 = 6000;
515
+ 나무죽이기(j.kid, { 파이프끊기: false, 상한: 넉넉히 });
443
516
  await 죽는말기다리기(j.kid, 죽는말기다림);
444
517
  /*
445
518
  * 아직 살아 있으면 여기서 끊는다.
@@ -447,10 +520,44 @@ export async function 끝내기(번호) {
447
520
  * 곧 목록에서 지울 참인데, 지우고 나면 이놈을 가리킬 방법이 없다. 안 죽은
448
521
  * 채로 지우면 영영 못 찾는 프로세스가 하나 남는다 — 이 기능이 없애려던
449
522
  * 바로 그 상태다. 유닉스에서 SIGTERM 을 안 듣는 놈이 여기로 온다.
523
+ * 윈도우에서는 나무를 다 못 훑어 뿌리를 살려 둔 경우가 여기로 온다.
524
+ *
525
+ * **뿌리살리기를 끈다.** 이걸 안 끄면 윈도우에서 taskkill 이 또 잘렸을 때
526
+ * 나무죽이기 가 kid.kill() 에 닿지도 못하고 그냥 돌아온다 — 「곧장」 이
527
+ * 윈도우에서는 아무 뜻이 없었다는 말이다. 여기는 마지막 자리라 뿌리라도
528
+ * 죽여야 한다. (모두끝내기 는 이미 그렇게 하고 있었다.)
450
529
  */
451
- 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
+ }
452
545
  파이프끊기(j.kid);
453
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
+ }
454
561
  일감들.delete(j.번호);
455
562
  return { 이미: false, 명령: j.명령, 초: j.산햇수(), 남은: 남은.글 };
456
563
  }
@@ -464,7 +571,14 @@ export async function 끝내기(번호) {
464
571
  export function 모두끝내기() {
465
572
  const n = [...일감들.values()].filter((j) => j.상태 === '도는중').length;
466
573
  for (const j of 일감들.values()) {
467
- if (j.상태 === '도는중') { j.상태 = '죽임'; 나무죽이기(j.kid); }
574
+ if (j.상태 !== '도는중') continue;
575
+ j.상태 = '죽임';
576
+ /*
577
+ * 여기는 프로그램이 끝나는 길이라 **다시 올 기회가 없다.** 한 번 짧게
578
+ * 훑어 보고, 잘렸으면 한 번 더 길게 훑는다. 그러고도 안 되면 뿌리라도
579
+ * 죽인다 — 아무것도 안 죽이는 것보다 낫다.
580
+ */
581
+ if (!나무죽이기(j.kid)) 나무죽이기(j.kid, { 상한: 4000, 뿌리살리기: false });
468
582
  }
469
583
  일감들.clear();
470
584
  return n;
@@ -637,6 +751,21 @@ export const JOBS_TOOL = {
637
751
  const r = await 끝내기(번호);
638
752
  if (!r) return { error: `${번호}번 일감이 없습니다. 번호 없이 Jobs 를 불러 목록을 보세요.` };
639
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
+ }
640
769
  return {
641
770
  content: `${번호}번을 끝냈습니다 (${r.초}초 돌았습니다): ${r.명령}`
642
771
  + (r.남은 ? `\n\n마지막 출력:\n${뒤만(r.남은, 2000).글}` : ''),