deel-local-cli 1.5.7 → 1.6.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 (57) hide show
  1. package/README.en.md +24 -13
  2. package/README.md +23 -12
  3. package/bin/deel.js +10 -0
  4. package/package.json +2 -2
  5. package/src/acp/map.js +143 -0
  6. package/src/acp/serve.js +160 -13
  7. package/src/agent/commit.js +511 -0
  8. package/src/agent/compact.js +269 -226
  9. package/src/agent/loop.js +154 -12
  10. package/src/agent/mention.js +56 -10
  11. package/src/agent/review.js +192 -0
  12. package/src/agent/session.js +20 -2
  13. package/src/agent/threads.js +1 -1
  14. package/src/backend/adapter.js +342 -280
  15. package/src/backend/azure.js +151 -0
  16. package/src/backend/ctxsize.js +19 -0
  17. package/src/backend/detect.js +106 -0
  18. package/src/backend/http.js +350 -30
  19. package/src/backend/probe.js +48 -1
  20. package/src/backend/proxy.js +151 -0
  21. package/src/backend/quota.js +133 -0
  22. package/src/backend/retry.js +132 -0
  23. package/src/backend/vision.js +185 -0
  24. package/src/commands.js +341 -8
  25. package/src/completion.js +264 -0
  26. package/src/config.js +117 -1
  27. package/src/i18n/en.js +13 -1
  28. package/src/i18n/index.js +22 -3
  29. package/src/i18n/ja.js +266 -0
  30. package/src/i18n/ko.js +12 -0
  31. package/src/i18n/zh.js +266 -0
  32. package/src/oneshot.js +28 -3
  33. package/src/pack/sbom.js +25 -0
  34. package/src/pack/selfpack.js +9 -3
  35. package/src/plugins/manage.js +34 -7
  36. package/src/repl.js +113 -15
  37. package/src/report.js +26 -2
  38. package/src/safety/guard.js +82 -3
  39. package/src/safety/keystore.js +237 -0
  40. package/src/safety/network.js +76 -14
  41. package/src/safety/policy.js +209 -0
  42. package/src/safety/secrets.js +32 -0
  43. package/src/setup.js +40 -7
  44. package/src/tools/clipboard.js +178 -0
  45. package/src/tools/fastgrep.js +217 -0
  46. package/src/tools/fsutil.js +55 -12
  47. package/src/tools/ignore.js +192 -0
  48. package/src/tools/index.js +282 -39
  49. package/src/tools/jobs.js +4 -12
  50. package/src/tools/outline.js +3 -1
  51. package/src/tools/pdf.js +1326 -0
  52. package/src/tools/shell.js +123 -0
  53. package/src/tools/verify.js +20 -4
  54. package/src/tools/webfetch.js +18 -5
  55. package/src/ui/inputbox.js +2 -2
  56. package/src/ui/screen.js +4 -1
  57. package/src/ui/status.js +22 -0
package/src/acp/serve.js CHANGED
@@ -27,15 +27,22 @@
27
27
  // 그래서 여기서 process.stdout.write 를 통째로 바꿔 끼운다. 부르는 자리를
28
28
  // 하나하나 찾아 막는 방법도 있지만, 그건 앞으로 새로 쓰는 코드까지 계속
29
29
  // 조심해야 한다는 뜻이다 — 언젠가 반드시 한 군데를 빠뜨린다.
30
+ import { existsSync } from 'node:fs';
31
+ import { join } from 'node:path';
30
32
  import { VERSION } from '../version.js';
33
+ import { 규칙모으기, 늘허락 } from '../safety/policy.js';
31
34
  import { run } from '../agent/loop.js';
32
- import { Session } from '../agent/session.js';
35
+ import { Session, repairToolPairs } from '../agent/session.js';
36
+ import { Store, sessionsDir, prune } from '../agent/store.js';
33
37
  import { makeScope } from '../safety/guard.js';
34
38
  import { History } from '../safety/undo.js';
35
39
  import { Audit } from '../safety/audit.js';
36
- import { activeProfile, load, resolveKey, homeDir } from '../config.js';
40
+ import { activeProfile, load, resolveKey, homeDir, save as saveCfg } from '../config.js';
41
+ import { 말 as 옮긴말 } from '../i18n/index.js';
42
+ import { 알림채움 } from '../backend/retry.js';
37
43
  import { discover } from '../skills/discover.js';
38
44
  import { allowEndpoint, setOffline } from '../safety/network.js';
45
+ import { 주소가리기 } from '../safety/secrets.js';
39
46
  import { probeCtx, 기본값 as CTX_DEFAULT } from '../backend/ctxsize.js';
40
47
  import { 다붙이기 } from '../backend/mcp.js';
41
48
  import { 배움 } from '../agent/evolve.js';
@@ -45,7 +52,7 @@ import { route } from '../agent/route.js';
45
52
  import { ORDER as 모드순서, get as getWork, normalize as 모드정리 } from '../agent/modes.js';
46
53
  import { 모두끝내기 as 일감모두끝내기 } from '../tools/jobs.js';
47
54
  import { 연결, 줄나누기, 모르는방법오류, 잘못된인자오류 } from './jsonrpc.js';
48
- import { 도구시작, 도구끝남, 도구이름표, 도구갈래, 도구자리, 멈춘까닭, 프롬프트글 } from './map.js';
55
+ import { 도구시작, 도구끝남, 도구이름표, 도구갈래, 도구자리, 멈춘까닭, 프롬프트글, 되살린것 } from './map.js';
49
56
 
50
57
  /** 우리가 말하는 규격 판. 정수 하나이고, 깨지는 변경에서만 올라간다. */
51
58
  export const 규격판 = 1;
@@ -79,7 +86,6 @@ export async function acp(opts = {}) {
79
86
 
80
87
  /** 세션 하나. 에디터의 탭 하나에 해당한다. */
81
88
  const 방들 = new Map();
82
- let 다음방번호 = 1;
83
89
  let 클라이언트 = null; // initialize 로 받은 저쪽 소개
84
90
  let 시작했나 = false;
85
91
 
@@ -89,7 +95,12 @@ export async function acp(opts = {}) {
89
95
  });
90
96
 
91
97
  // ── 방 만들기 ─────────────────────────────────────────────────────────
92
- async function 방만들기(요청) {
98
+ /**
99
+ * @param {object} 요청 session/new 또는 session/load 로 온 인자
100
+ * @param {object} o
101
+ * @param {string|null} o.아이디 이어할 대화 이름. 주면 그 파일에 이어 쓴다
102
+ */
103
+ async function 방만들기(요청, { 아이디 = null } = {}) {
93
104
  const cfg = load();
94
105
  const prof = activeProfile(cfg);
95
106
  if (!prof) {
@@ -106,6 +117,19 @@ export async function acp(opts = {}) {
106
117
  */
107
118
  const root = typeof 요청?.cwd === 'string' && 요청.cwd ? 요청.cwd : (opts.root ?? process.cwd());
108
119
 
120
+ /*
121
+ * 대화는 폴더 안(.deel/sessions/)에 남는다 — 터미널에서 하던 것과 같은 자리다.
122
+ *
123
+ * 세션 이름을 그 파일 이름으로 삼는 데는 까닭이 있다. ACP 의 sessionId 는
124
+ * 에디터가 적어 뒀다가 **다음에 켤 때 session/load 로 도로 들고 오는** 이름이다.
125
+ * 'deel-1' 처럼 이 프로세스 안에서만 뜻이 있는 번호를 주면, 껐다 켠 순간 그
126
+ * 이름이 가리키는 것이 아무 데도 없다. 그러면 loadSession 은 못 지킬 약속이 된다.
127
+ */
128
+ if (아이디 && !existsSync(join(sessionsDir(root), `${아이디}.jsonl`))) {
129
+ throw 잘못된인자오류(`그런 대화가 없습니다: ${아이디} (${root})`);
130
+ }
131
+ const store = new Store(root, 아이디);
132
+
109
133
  const conn = {
110
134
  kind: prof.kind, base: prof.baseUrl, auth: prof.auth,
111
135
  key: resolveKey(prof), model: prof.model,
@@ -115,7 +139,8 @@ export async function acp(opts = {}) {
115
139
  tools: prof.tools ?? false, json: prof.json ?? false, think: prof.think ?? false,
116
140
  };
117
141
  allowEndpoint(conn.base);
118
- if (opts.offline ?? prof.offline) setOffline(true);
142
+ // 관리 정책이 offline 을 못박아 뒀으면 옵션과 상관없이 켠다 (safety/policy.js).
143
+ if (opts.offline ?? prof.offline ?? cfg?.offline) setOffline(true);
119
144
 
120
145
  const session = new Session(conn, {
121
146
  root,
@@ -133,6 +158,28 @@ export async function acp(opts = {}) {
133
158
  session.plugins = found.plugins;
134
159
  session.못박은것 = new 못박기();
135
160
 
161
+ /*
162
+ * 이어하기라면 적힌 것을 도로 채워 넣는다.
163
+ *
164
+ * repairToolPairs 를 반드시 거친다. 도구가 도는 중에 프로그램이 끊겼으면
165
+ * 부름만 적히고 답이 없는데, 그대로 보내면 규격 서버가 400 을 준다 —
166
+ * 되살리자마자 첫 마디에서 죽는다. 터미널의 --resume 과 같은 손질이다.
167
+ */
168
+ if (아이디) {
169
+ const { messages: 적힌것 } = store.load();
170
+ const { messages, 고친것 } = repairToolPairs(적힌것);
171
+ session.messages = messages;
172
+ // 못 박아 둔 것도 같이 되살린다 (agent/pins.js). 이걸 빠뜨리면 '접어도
173
+ // 안 지워진다' 가 에디터를 닫았다 여는 한 번에 거짓이 된다.
174
+ const 박힌것 = store.못박은것읽기();
175
+ if (박힌것.length) session.못박은것 = new 못박기(박힌것);
176
+ 로그(`${store.id} — 메시지 ${messages.length}개를 이어 받았습니다${고친것 ? ` (끊긴 도구 호출 ${고친것}개는 걷어냈습니다)` : ''}.`);
177
+ }
178
+ // 주소는 가려서 적는다. 열쇠가 주소에 박혀 오는 게이트웨이가 있어서,
179
+ // 그대로 적으면 대화 기록 파일에 열쇠가 남는다 (safety/secrets.js).
180
+ store.begin({ model: conn.model, base: 주소가리기(conn.base), root });
181
+ try { prune(root); } catch { /* 정리는 못 해도 대화는 된다 */ }
182
+
136
183
  /*
137
184
  * 밖에서 붙인 도구(MCP).
138
185
  *
@@ -154,8 +201,8 @@ export async function acp(opts = {}) {
154
201
  }
155
202
 
156
203
  const 방 = {
157
- id: `deel-${다음방번호++}`,
158
- root, conn, session,
204
+ id: store.id,
205
+ root, conn, session, store,
159
206
  턴: null,
160
207
  늘허락: new Set(), // 이 세션에서 "앞으로 묻지 않기" 를 고른 도구들
161
208
  mcp: mcp붙임.서버들,
@@ -167,6 +214,10 @@ export async function acp(opts = {}) {
167
214
  방.ctx = {
168
215
  scope: makeScope(root),
169
216
  get 모델컨텍스트() { return conn.ctx ?? null; },
217
+ // 이 모델이 그림을 볼 수 있나 — Read 가 그림을 만났을 때 무슨 말을 할지가 여기서 갈린다.
218
+ get 눈있나() { return !!conn.vision; },
219
+ // 적어 둔 허락·금지 규칙 (safety/policy.js). 승인 모드보다 먼저 본다.
220
+ 규칙들: 규칙모으기(cfg),
170
221
  history: new History(root),
171
222
  audit: new Audit(root),
172
223
  seen: new Set(),
@@ -244,7 +295,25 @@ export async function acp(opts = {}) {
244
295
 
245
296
  const 결과 = 답?.outcome ?? {};
246
297
  if (결과.outcome !== 'selected') return false; // cancelled 도 여기로 온다
247
- if (결과.optionId === 'allow_always') { 방.늘허락.add(이름); return true; }
298
+ if (결과.optionId === 'allow_always') {
299
+ 방.늘허락.add(이름);
300
+ /*
301
+ * '앞으로 묻지 않기' 는 **다음에 켤 때도** 안 물어야 한다.
302
+ *
303
+ * 이 켠 동안만 기억하면 사람은 편집기를 닫았다 열 때마다 같은 것을
304
+ * 다시 허락한다. 그러면 그 버튼은 '앞으로' 가 아니라 '이번 켠 동안' 이다.
305
+ * 설정에 적어 두고, 다음부터는 규칙이 먼저 통과시킨다 (safety/policy.js).
306
+ *
307
+ * 금지에 걸리는 것은 안 적는다 — 적어 봐야 금지가 이기는데 목록에는
308
+ * 허락으로 보여서, 사람이 풀린 줄 안다.
309
+ */
310
+ try {
311
+ const cfg = load();
312
+ const r = 늘허락(cfg, 이름, 규칙모으기(cfg));
313
+ if (r.ok) saveCfg(cfg);
314
+ } catch { /* 못 적어도 이번 켠 동안은 먹는다 */ }
315
+ return true;
316
+ }
248
317
  return 결과.optionId === 'allow_once';
249
318
  } catch (err) {
250
319
  /*
@@ -292,6 +361,19 @@ export async function acp(opts = {}) {
292
361
  let 왜 = '';
293
362
  방.메시지번호++;
294
363
 
364
+ /*
365
+ * 어디까지 적었나. 자주 흘려 보낸다.
366
+ *
367
+ * 턴이 다 끝나고 한 번에 적으면, 도구를 열 번 돌린 뒤에 에디터가 닫힌 순간
368
+ * 그 열 번이 통째로 사라진다. 되살릴 때 제일 아쉬운 것이 바로 그 부분이라
369
+ * (무엇을 이미 확인했는지) 걸음마다 적는다.
370
+ */
371
+ let 적은데까지 = 방.session.messages.length;
372
+ const 적기 = () => {
373
+ for (const m of 방.session.messages.slice(적은데까지)) 방.store.append(m);
374
+ 적은데까지 = 방.session.messages.length;
375
+ };
376
+
295
377
  try {
296
378
  for await (const ev of run(방.session, 방.ctx, 글, { signal: 턴.signal })) {
297
379
  switch (ev.type) {
@@ -320,6 +402,11 @@ export async function acp(opts = {}) {
320
402
  말하기(`\n\n_(${ev.why} — 다시 답을 받습니다)_\n\n`);
321
403
  break;
322
404
 
405
+ // 서버가 잠깐 막아 기다리는 중. 아직 흘러간 글이 없으니 답을 새로 시작하지는 않는다.
406
+ case 'backoff':
407
+ 말하기(`\n\n_(${옮긴말('loop.backoff', 알림채움(ev))})_\n\n`);
408
+ break;
409
+
323
410
  case 'tool_start':
324
411
  보내기(도구시작(도구맡기기(방, ev.name), ev.name, ev.args));
325
412
  break;
@@ -332,6 +419,18 @@ export async function acp(opts = {}) {
332
419
 
333
420
  case 'tool':
334
421
  보내기(도구끝남(도구찾기(방, ev.name), ev));
422
+ 적기();
423
+ break;
424
+
425
+ /*
426
+ * 접히면 이력이 통째로 바뀐다.
427
+ *
428
+ * 덧붙이기로는 못 맞춘다 — 앞의 것 여러 개가 요약 하나로 바뀌었으니,
429
+ * 이어 붙이면 파일에는 접히기 전과 접힌 뒤가 겹쳐 남는다. 새로 적는다.
430
+ */
431
+ case 'compacted':
432
+ 방.store.replace(방.session.messages, `압축 — ${ev.folded}개를 요약으로`);
433
+ 적은데까지 = 방.session.messages.length;
335
434
  break;
336
435
 
337
436
  /*
@@ -401,6 +500,9 @@ export async function acp(opts = {}) {
401
500
  } finally {
402
501
  if (방.턴 === 턴) 방.턴 = null;
403
502
  방.돌던도구.clear();
503
+ // 끊겼든 터졌든 여기까지 오간 것은 남긴다. 끊긴 턴이야말로 다음에
504
+ // 되살려서 이어가고 싶은 자리다.
505
+ try { 적기(); } catch { /* 못 적어도 답은 돌려줘야 한다 */ }
404
506
  }
405
507
 
406
508
  /*
@@ -454,10 +556,9 @@ export async function acp(opts = {}) {
454
556
  // 저쪽이 우리보다 새 판을 말하면 우리 판을 답한다. 규격이 그렇게 정했다.
455
557
  protocolVersion: Number.isFinite(저쪽판) && 저쪽판 < 규격판 ? 저쪽판 : 규격판,
456
558
  agentCapabilities: {
457
- // session/load 아직 한다. 지난 대화를 되살리려면 오간 말을
458
- // 전부 session/update 다시 흘려야 하는데, 자리를 반쯤 만들어
459
- // 두면 에디터가 빈 대화를 열고 사용자는 기록이 날아간 줄 안다.
460
- loadSession: false,
559
+ // 지난 대화를 되살릴 있다. 값이 false 에디터는 아예 안
560
+ // 물어보고 대화를 연다 붙여 놓고 켜면 없는 것과 같다.
561
+ loadSession: true,
461
562
  promptCapabilities: { image: false, audio: false, embeddedContext: true },
462
563
  mcpCapabilities: { http: false, sse: false },
463
564
  },
@@ -478,6 +579,52 @@ export async function acp(opts = {}) {
478
579
  };
479
580
  }
480
581
 
582
+ /*
583
+ * 지난 대화 되살리기.
584
+ *
585
+ * ── 답에 대화를 실어 주는 자리가 없다 ────────────────────────────
586
+ *
587
+ * 규격은 오간 말을 **session/update 로 전부 다시 흘리라**고 한다. 답으로
588
+ * 한 번에 주는 자리가 아예 없다. 그래서 여기서 흘린 것이 에디터에 그려지는
589
+ * 지난 대화의 전부다.
590
+ *
591
+ * 흘리기 전에 방을 다 만들어야 한다. 반쯤 만들어 두고 흘리다가 설정이
592
+ * 없어서 터지면, 에디터에는 지난 대화가 절반만 그려진 채로 남는다.
593
+ */
594
+ case 'session/load': {
595
+ const 아이디 = String(인자?.sessionId ?? '').trim();
596
+ if (!아이디) throw 잘못된인자오류('sessionId 가 없습니다.');
597
+
598
+ // 이 프로세스가 이미 열어 둔 방이면 그대로 쓴다. 다시 만들면 같은
599
+ // 대화를 두 곳에서 밟게 되고, 그때부터 어느 쪽이 참인지 알 수 없다.
600
+ const 방 = 방들.get(아이디) ?? await 방만들기(인자, { 아이디 });
601
+
602
+ /*
603
+ * 화면에 그릴 것과 모델에게 줄 것이 다르다.
604
+ *
605
+ * 모델에게는 손질한 것이 간다 — 답 없는 도구 부름이 섞이면 규격 서버가
606
+ * 400 을 준다. 그런데 **사람에게는 그 자리를 보여 줘야 한다.** 어제
607
+ * 무엇을 하다 끊겼는지가 오늘 무엇을 시킬지를 정하기 때문이다. 그래서
608
+ * 그리는 것은 적힌 그대로(손질 전)를 쓴다.
609
+ *
610
+ * 파일을 다시 읽는 까닭도 같다. 걸음마다 적어 두므로 파일이 늘 최신이고,
611
+ * 손질로 걷어낸 것까지 그대로 남아 있는 유일한 자리다.
612
+ */
613
+ let 그릴것 = 방.session.messages;
614
+ try {
615
+ const 적힌것 = 방.store.load().messages;
616
+ if (적힌것.length) 그릴것 = 적힌것;
617
+ } catch { /* 못 읽으면 기억하고 있는 것으로 그린다 */ }
618
+
619
+ let 흘린것 = 0;
620
+ for (const update of 되살린것(그릴것)) {
621
+ 관.알림('session/update', { sessionId: 방.id, update });
622
+ 흘린것++;
623
+ }
624
+ 로그(`${방.id} — 지난 대화 ${흘린것}덩이를 되살렸습니다.`);
625
+ return { modes: 모드상태(방) };
626
+ }
627
+
481
628
  case 'session/prompt': {
482
629
  const 방 = 방찾기(인자?.sessionId);
483
630
  return await 한턴(방, 인자?.prompt);