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
@@ -14,7 +14,7 @@
14
14
  // 마지막 줄까지는 성하다. 통째로 다시 쓰는 방식이면 그 순간 파일이 깨진다.
15
15
  import {
16
16
  existsSync, mkdirSync, readdirSync, readFileSync,
17
- appendFileSync, writeFileSync, statSync, rmSync,
17
+ appendFileSync, writeFileSync, statSync, rmSync, chmodSync,
18
18
  } from 'node:fs';
19
19
  import { join } from 'node:path';
20
20
 
@@ -51,19 +51,93 @@ export class Store {
51
51
  this.auto = id == null; // 우리가 지은 이름인가 (그러면 겹칠 때 바꿔도 된다)
52
52
  this.#use(id ?? freeId(this.dir));
53
53
  this.opened = false;
54
+ /** 적으려다 못 적은 줄 수와 첫 까닭. 0 이 아니면 이 대화는 반만 남는다. */
55
+ this.못쓴수 = 0;
56
+ this.못쓴까닭 = null;
57
+ this.말한적있나 = false;
58
+ // 이 파일과 묶인 대화, 그리고 그 대화에서 **마지막으로 적어 둔** 할 일·시킨 말.
59
+ // 안 바뀐 것을 다시 적지 않으려고 들고 있는다 (살림적기).
60
+ this.따라갈세션 = null;
61
+ this.적은할일 = null;
62
+ this.적은요청 = null;
54
63
  }
55
64
 
56
65
  #use(id) {
57
66
  this.id = id;
58
67
  this.file = join(this.dir, `${id}.jsonl`);
68
+ this.#잠갔나 = false; // 다른 파일로 옮겨 갔으면 그 파일은 아직 안 잠갔다
69
+ }
70
+
71
+ /*
72
+ * 만든 뒤 본인만 읽게 잠근다 (config.js 가 설정 파일에 하는 것과 같다).
73
+ *
74
+ * 여기 남는 것은 대화 전체다 — 사람이 붙여 넣은 글, 읽은 파일 내용, 모델이
75
+ * 쓴 답. 홈이 공유 폴더에 있거나 같은 PC 를 여럿이 쓰면 그게 그대로 읽힌다.
76
+ * 한 번만 건다. 윈도우(NTFS)에서는 chmod 가 아무 일도 안 한다 — 거기서는
77
+ * 못 잠근 것이지 잠근 척하지 않는다.
78
+ *
79
+ * 건 결과를 `잠금` 에 남긴다. "본인만 읽게 잠근다" 는 말은 지켜지는지
80
+ * **밖에서 볼 수 있어야** 뜻이 있다 (keystore.js 의 마지막명령줄 과 같은
81
+ * 까닭). 윈도우에서 값이 남는다고 실제로 잠겼다는 뜻은 아니다.
82
+ */
83
+ #잠갔나 = false;
84
+ /** 파일 권한을 건 결과. `{모드}` 면 걸었고, `{못함}` 이면 못 걸었다. */
85
+ 잠금 = null;
86
+ #잠그기() {
87
+ if (this.#잠갔나) return;
88
+ this.#잠갔나 = true;
89
+ try { chmodSync(this.file, 0o600); this.잠금 = { 모드: 0o600 }; }
90
+ catch (err) { this.잠금 = { 못함: err?.code ?? String(err) }; }
59
91
  }
60
92
 
61
93
  #open() {
62
94
  if (this.opened) return;
63
- mkdirSync(this.dir, { recursive: true });
95
+ // 폴더를 만들어도 여기서 던지지 않는다. 던지면 대화 자체가 그 자리에서
96
+ // 끊긴다 — 기록을 남기려다 작업을 죽이면 본말이 뒤집힌다.
97
+ // 조용히 넘기는 것도 아니다: 폴더가 없으면 바로 아래 쓰기가 깨지고,
98
+ // 그 자리에서 세어 화면에 오른다.
99
+ try { mkdirSync(this.dir, { recursive: true }); } catch { /* 아래 쓰기가 센다 */ }
64
100
  this.opened = true;
65
101
  }
66
102
 
103
+ /*
104
+ * 못 적은 것을 세어 둔다.
105
+ *
106
+ * 이 파일 첫 줄이 파는 문장이 「껐다 켜도 이어서 하게 한다」 이고, /sessions
107
+ * 화면은 대놓고 「지금 대화는 나가지 않아도 계속 저장되고 있습니다」 라고
108
+ * 적어 준다. 디스크가 차거나 홈이 읽기 전용이거나 파일을 누가 잡고 있으면
109
+ * 그 두 문장이 거짓이 되는데, 여태 화면은 아무 말이 없었다.
110
+ *
111
+ * 그 침묵이 비싼 까닭: 사람이 알아차리는 자리가 **다음 날 --resume** 이다.
112
+ * 그때는 이미 대화가 없고, 되돌릴 방법도 없다. 안 적히고 있다는 것만 그때
113
+ * 알았어도 창을 안 닫거나 중요한 것을 따로 적어 뒀을 것이다.
114
+ * 감사기록(safety/audit.js)에서 한 것과 같은 방식으로 세어 둔다.
115
+ */
116
+ #못썼다(err) {
117
+ this.못쓴수 += 1;
118
+ // 까닭은 코드가 먼저다 — EACCES·ENOSPC 한 낱말이 긴 문장보다 알아보기 쉽고,
119
+ // 파일 경로가 섞여 들어가지 않아 화면 한 줄에 들어간다.
120
+ if (!this.못쓴까닭) this.못쓴까닭 = err?.code ?? err?.message ?? String(err);
121
+ }
122
+
123
+ /** 저장이 새고 있나. 새고 있으면 `{수, 까닭}`, 멀쩡하면 null. */
124
+ 못쓴것() {
125
+ return this.못쓴수 ? { 수: this.못쓴수, 까닭: this.못쓴까닭 } : null;
126
+ }
127
+
128
+ /**
129
+ * 화면이 **한 번만** 말하게 하려고 쓴다. 처음 물어볼 때만 알려 주고 그 뒤로는 null.
130
+ *
131
+ * 한 줄 못 적을 때마다 경고를 찍으면 사람은 이틀 만에 그 줄을 안 읽게 되고,
132
+ * 그러면 정작 처음 한 번도 못 읽힌다. 세어 둔 값(못쓴것)은 안 지운다 —
133
+ * /sessions 화면은 언제 열어도 지금 몇 건인지 말해야 한다.
134
+ */
135
+ 처음못쓴것() {
136
+ if (!this.못쓴수 || this.말한적있나) return null;
137
+ this.말한적있나 = true;
138
+ return this.못쓴것();
139
+ }
140
+
67
141
  /**
68
142
  * 첫 줄은 머리글 — 어떤 모델·어떤 폴더에서 한 대화인지.
69
143
  *
@@ -77,9 +151,12 @@ export class Store {
77
151
  for (let tries = 0; tries < 50; tries++) {
78
152
  try {
79
153
  writeFileSync(this.file, head, { encoding: 'utf8', flag: 'wx' });
154
+ this.#잠그기();
80
155
  return this;
81
156
  } catch (err) {
82
- if (err?.code !== 'EEXIST') return this; // 못 적어도 대화는 계속돼야 한다
157
+ // 못 적어도 대화는 계속돼야 한다. 다만 조용히 넘기지는 않는다 —
158
+ // 머리글을 못 적었으면 그 뒤의 줄도 십중팔구 못 적는다.
159
+ if (err?.code !== 'EEXIST') { this.#못썼다(err); return this; }
83
160
  if (!this.auto) return this; // 이어쓰기 — 이미 있는 게 맞다
84
161
  this.#use(freeId(this.dir)); // 누가 채 갔다. 옆자리로.
85
162
  }
@@ -88,14 +165,78 @@ export class Store {
88
165
  }
89
166
 
90
167
  #write(obj) {
91
- try { appendFileSync(this.file, JSON.stringify(obj) + '\n', 'utf8'); }
92
- catch { /* 기록에 실패해도 대화는 계속돼야 한다 */ }
168
+ try {
169
+ appendFileSync(this.file, JSON.stringify(obj) + '\n', 'utf8');
170
+ this.#잠그기();
171
+ } catch (err) { this.#못썼다(err); } // 계속은 하되, 몇 줄을 잃었는지는 센다
93
172
  }
94
173
 
95
174
  // 메시지 하나를 덧붙인다. 대화가 진행되는 대로 즉시 남긴다.
96
175
  append(msg) {
97
176
  this.#open();
98
177
  this.#write({ t: 'msg', m: msg });
178
+ // 남은 할 일과 시킨 말도 이 자리에서 같이 본다 (아래 살림적기).
179
+ // 따로 부르는 자리를 만들면 언젠가 한 길에서만 부르게 되고, 그 길로 들어온
180
+ // 사람만 이어하기가 반쪽이 된다.
181
+ this.살림적기();
182
+ }
183
+
184
+ /*
185
+ * ── 남은 할 일과 시킨 말 원문 ──────────────────────────────────────────
186
+ *
187
+ * 1.9.2 는 **한 대화 안에서** 이 둘이 접히거나 줄어들며 사라지던 것을 막았다
188
+ * (session.js 의 못박을것). 그런데 둘 다 세션 객체, 곧 메모리에만 있어서
189
+ * 창을 닫는 한 번에 그 보호가 통째로 없어졌다 — 이어받으면 오간 말은 돌아오고
190
+ * 못 박은 것도 돌아오는데, 남은 할 일과 시킨 말 원문만 안 돌아온다.
191
+ * 「요청사항이 많으면 몇 개 까먹는다」 가 껐다 켜는 자리에서 되풀이된다.
192
+ *
193
+ * 메시지와 따로 한 줄로 두는 이유는 pins 와 같다 — 접기·요약은 메시지를
194
+ * 손보는데 이 둘은 그 손질에 닿으면 안 된다. 마지막 것이 이긴다.
195
+ */
196
+
197
+ /**
198
+ * 이 대화의 할 일·시킨 말을 이 저장 파일과 묶는다.
199
+ *
200
+ * 처음 묶을 때 파일에 적힌 것을 세션으로 되살린다 — 이어하기의 빠져 있던
201
+ * 절반이 여기다. 그 뒤로는 append 가 돌 때마다 **바뀐 만큼만** 적는다.
202
+ */
203
+ 살림따라가기(session) {
204
+ if (!session) return this;
205
+ const 처음 = this.따라갈세션 !== session;
206
+ this.따라갈세션 = session;
207
+ if (처음) this.#살림되살리기(session);
208
+ return this;
209
+ }
210
+
211
+ #살림되살리기(session) {
212
+ const { 할일, 이번요청 } = this.load();
213
+ // 적힌 것이 없으면 세션이 들고 있던 것을 그대로 둔다. 빈 값으로 덮으면
214
+ // 새 갈래를 열자마자 방금 시킨 말이 사라진다 — 갈래마다 파일이 따로다.
215
+ if (할일) session.할일 = 할일;
216
+ if (이번요청) session.이번요청 = 이번요청;
217
+ this.적은할일 = JSON.stringify(session.할일 ?? []);
218
+ this.적은요청 = String(session.이번요청 ?? '');
219
+ }
220
+
221
+ /**
222
+ * 바뀐 것만 적는다. 안 바뀌었으면 한 줄도 안 늘린다.
223
+ *
224
+ * 메시지마다 목록을 통째로 다시 적으면 긴 대화에서 파일이 몇 배가 되고,
225
+ * 그 값은 매번 사람이 기다리는 시간이다. pins 를 사람이 고칠 때만 적는
226
+ * 것과 같은 뜻이다.
227
+ */
228
+ 살림적기(session = this.따라갈세션) {
229
+ if (!session) return;
230
+ const 할일 = JSON.stringify(session.할일 ?? []);
231
+ if (할일 !== this.적은할일) {
232
+ this.적은할일 = 할일;
233
+ this.#write({ t: 'todo', at: new Date().toISOString(), 목록: session.할일 ?? [] });
234
+ }
235
+ const 요청 = String(session.이번요청 ?? '');
236
+ if (요청 !== this.적은요청) {
237
+ this.적은요청 = 요청;
238
+ this.#write({ t: 'request', at: new Date().toISOString(), 글: 요청 });
239
+ }
99
240
  }
100
241
 
101
242
  /**
@@ -132,11 +273,31 @@ export class Store {
132
273
  // 여기가 놓치기 쉬운 자리다. 파일을 새로 쓰면서 못 박은 것을 안 옮기면,
133
274
  // '요약해도 안 지워진다' 는 말이 바로 그 요약에서 거짓이 된다.
134
275
  const 못박은것 = this.못박은것읽기();
276
+ // 남은 할 일과 시킨 말 원문도 같은 이유로 옮겨 싣는다. 접힌 자리에서
277
+ // 다시 박으라고 들고 있는 것들인데, 정작 접는 자리에서 파일에서 빠지면
278
+ // 이어받을 때 그 둘만 없는 대화가 된다.
279
+ //
280
+ // 옮겨 싣기 **전에** 아직 안 적은 것을 먼저 적는다. 여기서 파일만 보고
281
+ // 옮기면, 메시지가 한 번도 안 붙은 사이에 할 일이 바뀐 경우 그 바뀐 것이
282
+ // 통째로 사라진다 — 접는 자리는 원래 제일 크게 잃는 자리다.
283
+ this.살림적기();
284
+ const { 할일, 이번요청 } = this.load();
135
285
  const lines = [JSON.stringify({ t: 'meta', at: new Date().toISOString(), ...meta })];
136
286
  lines.push(JSON.stringify({ t: 'note', at: new Date().toISOString(), note }));
137
287
  if (못박은것.length) lines.push(JSON.stringify({ t: 'pins', at: new Date().toISOString(), 목록: 못박은것 }));
288
+ if (할일) lines.push(JSON.stringify({ t: 'todo', at: new Date().toISOString(), 목록: 할일 }));
289
+ if (이번요청) lines.push(JSON.stringify({ t: 'request', at: new Date().toISOString(), 글: 이번요청 }));
138
290
  for (const m of messages) lines.push(JSON.stringify({ t: 'msg', m }));
139
- try { writeFileSync(this.file, lines.join('\n') + '\n', 'utf8'); } catch {}
291
+ /*
292
+ * 통째로 다시 쓰면 파일이 새로 만들어진다 — 그때 잠금도 다시 걸어야 한다.
293
+ * 그리고 여기서 깨지면 앞의 append 들이 남긴 것까지 통째로 못 고친 셈이
294
+ * 된다. 제일 크게 잃는 자리라 더더욱 조용히 넘길 수 없다.
295
+ */
296
+ try {
297
+ writeFileSync(this.file, lines.join('\n') + '\n', 'utf8');
298
+ this.#잠갔나 = false;
299
+ this.#잠그기();
300
+ } catch (err) { this.#못썼다(err); }
140
301
  }
141
302
 
142
303
  readMeta() {
@@ -148,19 +309,35 @@ export class Store {
148
309
  } catch { return null; }
149
310
  }
150
311
 
151
- // 저장된 대화를 읽어 온다. 깨진 줄은 건너뛴다 — 도중에 죽었을 수 있다.
312
+ /**
313
+ * 저장된 대화를 읽어 온다. 깨진 줄은 건너뛴다 — 도중에 죽었을 수 있다.
314
+ *
315
+ * 파일을 아예 못 여는 경우(권한, 다른 프로그램이 잡고 있음)에도 던지지 않는다.
316
+ * 이 함수는 list() 가 폴더의 대화를 하나씩 훑으면서 부른다. 한 파일에서
317
+ * 던지면 **목록 화면 전체가 안 뜨고**, 멀쩡히 이어할 수 있는 나머지 대화까지
318
+ * 사람 눈에서 사라진다. 못 읽은 것은 못읽음 에 까닭을 담아 알린다.
319
+ */
152
320
  load() {
153
321
  if (!existsSync(this.file)) return { meta: null, messages: [] };
322
+ let 글;
323
+ try { 글 = readFileSync(this.file, 'utf8'); }
324
+ catch (err) { return { meta: null, messages: [], 못읽음: err?.code ?? err?.message ?? String(err) }; }
154
325
  let meta = null;
155
326
  const messages = [];
156
- for (const line of readFileSync(this.file, 'utf8').split('\n')) {
327
+ // 마지막 것이 이긴다. 없으면 null 이고, null 은 '적힌 적이 없다' 는 뜻이라
328
+ // 빈 목록·빈 글과 구별된다 — 되살릴 때 그 차이로 덮을지 말지를 정한다.
329
+ let 할일 = null;
330
+ let 이번요청 = null;
331
+ for (const line of 글.split('\n')) {
157
332
  if (!line.trim()) continue;
158
333
  let j;
159
334
  try { j = JSON.parse(line); } catch { continue; }
160
335
  if (j.t === 'meta') meta = j;
161
336
  else if (j.t === 'msg' && j.m) messages.push(j.m);
337
+ else if (j.t === 'todo' && Array.isArray(j.목록)) 할일 = j.목록;
338
+ else if (j.t === 'request' && typeof j.글 === 'string') 이번요청 = j.글;
162
339
  }
163
- return { meta, messages };
340
+ return { meta, messages, 할일, 이번요청 };
164
341
  }
165
342
  }
166
343
 
@@ -40,7 +40,19 @@ export class Threads {
40
40
  this.session = session;
41
41
  this.ctx = ctx;
42
42
  this.새store = 새store;
43
- this.갈래들 = [{ 이름: '본줄기', messages: session.messages, usage: session.usage, todos: ctx?.todos ?? null, store: 첫store }];
43
+ /*
44
+ * 남은 할 일과 시킨 말 원문을 저장 파일과 묶는다 (agent/store.js 의 살림따라가기).
45
+ *
46
+ * **첫 한마디가 오기 전**인 여기여야 한다. 턴이 시작되면 loop.js 가
47
+ * 이번요청을 새 말로 덮으므로, 그 뒤에 되살리면 방금 시킨 말이 어제 시킨
48
+ * 말로 되돌아간다. 여태 이 둘만 메모리에 있어서, 이어받아도 오간 말은
49
+ * 돌아오는데 남은 할 일은 안 돌아왔다.
50
+ */
51
+ 첫store?.살림따라가기?.(session);
52
+ this.갈래들 = [{
53
+ 이름: '본줄기', messages: session.messages, usage: session.usage, todos: ctx?.todos ?? null,
54
+ 할일: session.할일, 이번요청: session.이번요청, store: 첫store,
55
+ }];
44
56
  this.자리 = 0;
45
57
  this.센것 = 1;
46
58
  }
@@ -55,6 +67,11 @@ export class Threads {
55
67
  g.messages = this.session.messages;
56
68
  g.usage = this.session.usage;
57
69
  g.todos = this.ctx?.todos ?? null;
70
+ // 남은 할 일·시킨 말도 갈래 것이다. 저장 파일이 갈래마다 따로라, 이걸 안
71
+ // 담아 두면 곁가지에서 적은 할 일을 들고 본줄기로 돌아와 본줄기 파일에
72
+ // 적어 버린다 — 이어받을 때 하지도 않은 일이 본줄기에 남는다.
73
+ g.할일 = this.session.할일;
74
+ g.이번요청 = this.session.이번요청;
58
75
  }
59
76
 
60
77
  /** 갈래 하나를 화면으로 꺼낸다. */
@@ -64,6 +81,10 @@ export class Threads {
64
81
  this.session.messages = g.messages;
65
82
  this.session.usage = g.usage;
66
83
  if (this.ctx) this.ctx.todos = g.todos;
84
+ this.session.할일 = g.할일 ?? [];
85
+ this.session.이번요청 = g.이번요청 ?? '';
86
+ // 적는 자리도 지금 갈래의 파일로 옮긴다 (agent/store.js 의 살림따라가기).
87
+ g.store?.살림따라가기?.(this.session);
67
88
  // 상태줄이 지금 어느 갈래인지 보여 줄 수 있게 남긴다.
68
89
  // 갈래가 하나뿐이면 안 보인다 — 안 쓰는 사람 화면은 그대로여야 한다.
69
90
  this.session.갈래표 = this.갈래들.length > 1 ? g.이름 : null;
@@ -84,6 +105,10 @@ export class Threads {
84
105
  messages: 물려줄것 ? [...물려줄것] : [],
85
106
  usage: { in: 0, out: 0, calls: 0, ms: 0, retries: 0 },
86
107
  todos: null,
108
+ // 할 일도 todos 와 같이 비워서 시작한다. 곁가지는 다른 일을 하러 나가는
109
+ // 것이고, 물려받으면 본줄기의 남은 일을 곁가지 파일에도 적게 된다.
110
+ 할일: [],
111
+ 이번요청: '',
87
112
  store,
88
113
  };
89
114
  this.갈래들.push(g);