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
@@ -7,12 +7,19 @@
7
7
  // - 막힘: 도구가 오류를 냈다. 얕게 생각하면 같은 실수를 또 한다. → 세게
8
8
  // 전부 똑같이 세게 두면 느리고, 전부 얕게 두면 엉뚱한 길로 간다.
9
9
 
10
+ import { 언어 } from '../i18n/index.js';
11
+
10
12
  export const LEVELS = ['off', 'low', 'medium', 'high', 'max'];
11
13
 
14
+ /*
15
+ * 단계 이름은 화면에 그대로 나간다. 그래서 영어 이름을 여기 같이 둔다 —
16
+ * modes.js 의 en 과 같은 방식이다. i18n 표에 같은 말을 또 적어 두면
17
+ * 단계를 하나 늘릴 때 한쪽만 늘어나서 화면에 빈칸이 뜬다.
18
+ */
12
19
  export const STAGES = {
13
- plan: { label: '첫 판단', why: '무엇을 할지 정하는 자리' },
14
- work: { label: '이어가기', why: '도구 결과를 읽고 다음 한 수' },
15
- fix: { label: '막혔을 때', why: '직전 도구가 오류를 냄' },
20
+ plan: { label: '첫 판단', why: '무엇을 할지 정하는 자리', en: 'First call', whyEn: 'Deciding what to do' },
21
+ work: { label: '이어가기', why: '도구 결과를 읽고 다음 한 수', en: 'Continue', whyEn: 'Reading a tool result, picking the next move' },
22
+ fix: { label: '막혔을 때', why: '직전 도구가 오류를 냄', en: 'Stuck', whyEn: 'The last tool returned an error' },
16
23
  };
17
24
 
18
25
  // shift = 기준 강도에서 몇 칸 올리고 내릴지.
@@ -40,6 +47,7 @@ export const PROFILES = {
40
47
  // i18n 표에 같은 말을 또 적어 두면 언젠가 둘이 갈라진다.
41
48
  en: 'even',
42
49
  desc: '모든 단계 같은 강도 — 예측 가능한 대신 느립니다',
50
+ descEn: 'Same effort at every stage - predictable, but slower',
43
51
  shift: { plan: 0, work: 0, fix: 0 },
44
52
  share: { plan: 0.40, work: 0.40, fix: 0.40 },
45
53
  },
@@ -47,6 +55,7 @@ export const PROFILES = {
47
55
  name: '절약',
48
56
  en: 'save',
49
57
  desc: '첫 판단만 세게, 이어가기는 얕게 — 대개 이게 낫습니다',
58
+ descEn: 'Hard on the first call, shallow while continuing - usually the better trade',
50
59
  shift: { plan: 0, work: -1, fix: +1 },
51
60
  share: { plan: 0.40, work: 0.35, fix: 0.45 },
52
61
  },
@@ -54,6 +63,7 @@ export const PROFILES = {
54
63
  name: '깊게',
55
64
  en: 'deep',
56
65
  desc: '전 단계 한 칸씩 위로 — 어려운 일에만',
66
+ descEn: 'Every stage one notch up - for hard work only',
57
67
  shift: { plan: +1, work: 0, fix: +1 },
58
68
  share: { plan: 0.50, work: 0.45, fix: 0.50 },
59
69
  },
@@ -158,16 +168,26 @@ export function wasCut(msg) {
158
168
  export function table(base, profileKey, room = {}) {
159
169
  const key = normalizeProfile(profileKey) ?? 'save';
160
170
  const p = PROFILES[key];
171
+ /*
172
+ * 화면 말이 한국어가 아니면 영어 이름으로 낸다.
173
+ *
174
+ * 여태 이 표는 언제나 한국어였다. /lang en 으로 켠 사람은 「단계 강도
175
+ * 출력상한」 옆에 「첫 판단·이어가기·막혔을 때」 가 서 있는 화면을 봤다 —
176
+ * 표의 틀만 영어고 알맹이가 한국어면 아무것도 안 읽힌다.
177
+ *
178
+ * 일본어·중국어도 영어로 간다. i18n 의 물러날곳(ja→en→ko)과 같은 규칙이다.
179
+ */
180
+ const 한국어 = 언어() === 'ko';
161
181
  return {
162
182
  key,
163
- name: p.name,
164
- desc: p.desc,
183
+ name: 한국어 ? p.name : (p.en ?? p.name),
184
+ desc: 한국어 ? p.desc : (p.descEn ?? p.desc),
165
185
  ctx: room.ctx ?? 0,
166
186
  used: room.used ?? 0,
167
187
  rows: Object.entries(STAGES).map(([stage, s]) => ({
168
188
  stage,
169
- label: s.label,
170
- why: s.why,
189
+ label: 한국어 ? s.label : (s.en ?? s.label),
190
+ why: 한국어 ? s.why : (s.whyEn ?? s.why),
171
191
  level: effortFor(base, key, stage),
172
192
  cap: tokensFor(key, stage, room),
173
193
  share: p.share[stage],
@@ -112,6 +112,8 @@ export function 증거모으기(session, { audit = null, 최근 = 400, 바꾼때
112
112
  바꾼것,
113
113
  돌린것,
114
114
  증명안된것,
115
+ // 기록이 새고 있으면 이 보고서는 실제보다 짧다. 짧은 채로 내보내면 안 된다.
116
+ 기록못씀: audit?.못쓴것?.() ?? null,
115
117
  // 화면이 한눈에 쓸 수 있게 미리 세어 둔다.
116
118
  셈: {
117
119
  파일: 바꾼것.length,
@@ -0,0 +1,141 @@
1
+ // 읽은 파일을 들고 있다가, 다시 읽을 때 **바뀐 만큼만** 싣는다.
2
+ //
3
+ // ── 무엇이 아까웠나 ─────────────────────────────────────────────────────
4
+ //
5
+ // 「폴더/파일을 읽으면 그 값을 가져와있다가 수정되면 수정하고, 필요하면
6
+ // 필요할때마다 해당 값을 써야 토큰이 덜 발생하는거 아니야?」
7
+ //
8
+ // 맞는 말이다. 여태 이랬다.
9
+ //
10
+ // Read src/a.js 1,200줄이 대화에 들어간다
11
+ // Edit src/a.js 한 줄 고친다
12
+ // Read src/a.js **1,200줄이 또 들어간다**
13
+ //
14
+ // 두 번째 1,200줄 중 실제로 새로운 것은 한 줄이다. 나머지 1,199줄은 이미
15
+ // 대화 안에 그대로 있다. 그런데 창은 그만큼 밀리고, 밀린 만큼 앞엣것이
16
+ // 접혀 나간다 — 돈을 두 번 내고 기억을 잃는 셈이다.
17
+ //
18
+ // loop.js 에도 되풀이를 막는 자리가 있지만 거기는 **결과가 똑같을 때**만
19
+ // 본다. 그리고 그 표는 턴 하나짜리다. 고치고 다시 읽는 것 — 제일 흔한
20
+ // 모양 — 은 거기에 안 걸린다.
21
+ //
22
+ // ── 지키는 선 ───────────────────────────────────────────────────────────
23
+ //
24
+ // **줄이는 것과 잃는 것은 다르다.** 「앞에서 읽은 그대로입니다」 는 앞엣것이
25
+ // 대화에 살아 있을 때만 참이다. 그래서 요약해서 접을 때(compact.js) 이
26
+ // 기억도 같이 버린다. 안 버리면 모델은 있지도 않은 글을 가리키는 쪽지만
27
+ // 받는다 — 그게 통째로 다시 싣는 것보다 훨씬 나쁘다.
28
+ //
29
+ // **부분만 읽은 것은 안 기억한다.** offset·limit 로 잘라 읽었거나 창이 좁아
30
+ // 앞부분만 준 경우, 우리가 들고 있는 것은 파일이 아니라 조각이다. 그걸
31
+ // 「그대로」 라고 하면 모델은 안 본 부분을 봤다고 여긴다.
32
+ import { diffLines } from '../ui/diff.js';
33
+
34
+ /*
35
+ * 바뀐 만큼만 싣는 것이 언제 이득인가.
36
+ *
37
+ * 조금 바뀌었으면 확실히 이득이다. 그런데 절반이 바뀌었으면 diff 가 원본보다
38
+ * 오히려 길어진다(뺀 줄과 넣은 줄이 둘 다 실리니까). 그때는 그냥 통째로
39
+ * 싣는 편이 짧고, 무엇보다 **읽기 쉽다.**
40
+ */
41
+ const 이득선 = 0.6;
42
+
43
+ /** 한 줄이라도 남기려면 이만큼은 있어야 한다 — 너무 짧은 파일은 diff 가 무의미하다. */
44
+ const 최소줄 = 40;
45
+
46
+ export class 파일기억 {
47
+ constructor() {
48
+ /** 경로 → { 글, 언제 } — 마지막으로 대화에 통째로 실은 내용. */
49
+ this.것들 = new Map();
50
+ }
51
+
52
+ /** 요약해서 접었다 = 앞엣것이 대화에서 사라졌다. 기억도 같이 버린다. */
53
+ 잊기() { this.것들.clear(); }
54
+
55
+ /**
56
+ * 이번에 실을 글을 정한다.
57
+ *
58
+ * @param {string} 경로 파일의 절대 경로
59
+ * @param {string} 새글 이번에 읽은 것(줄 번호가 붙은 그대로)
60
+ * @param {object} [옵션]
61
+ * @param {boolean} [옵션.부분인가] offset·limit·창 상한으로 잘라 읽었나
62
+ * @returns {{글: string, 어떻게: 'full'|'same'|'diff'}}
63
+ */
64
+ 실을것(경로, 새글, { 부분인가 = false } = {}) {
65
+ const 글 = String(새글 ?? '');
66
+ if (!경로 || 부분인가) return { 글, 어떻게: 'full' };
67
+
68
+ const 앞것 = this.것들.get(경로);
69
+ this.것들.set(경로, { 글, 언제: Date.now() });
70
+ if (!앞것) return { 글, 어떻게: 'full' };
71
+
72
+ if (앞것.글 === 글) {
73
+ return {
74
+ 글: '앞에서 읽은 그대로입니다 — 파일이 바뀌지 않았습니다.'
75
+ + ' 앞에 실린 내용을 그대로 쓰세요. (같은 글을 두 번 싣지 않습니다)',
76
+ 어떻게: 'same',
77
+ };
78
+ }
79
+
80
+ const 줄인것 = 바뀐데만(앞것.글, 글);
81
+ if (줄인것) return { 글: 줄인것, 어떻게: 'diff' };
82
+ return { 글, 어떻게: 'full' };
83
+ }
84
+ }
85
+
86
+ /*
87
+ * 줄 번호를 떼고 견준다.
88
+ *
89
+ * Read 는 ` 12\t코드` 처럼 줄 번호를 붙여 돌려준다. 그걸 그대로 견주면
90
+ * **맨 앞에 한 줄만 끼어도 그 아래 전부가 바뀐 줄로 잡힌다** — 번호가 다
91
+ * 밀리기 때문이다. 그러면 diff 가 원본보다 길어져서 줄이는 뜻이 사라진다.
92
+ * 그래서 번호를 떼고 견주고, 적을 때 **새 파일의 번호**로 다시 붙인다.
93
+ */
94
+ function 번호떼기(글) {
95
+ const 몸 = [];
96
+ const 번호 = [];
97
+ for (const l of String(글).split('\n')) {
98
+ const m = l.match(/^(\s*)(\d+)\t([\s\S]*)$/);
99
+ if (m) { 번호.push(m[2]); 몸.push(m[3]); } else { 번호.push(null); 몸.push(l); }
100
+ }
101
+ return { 몸: 몸.join('\n'), 번호 };
102
+ }
103
+
104
+ /**
105
+ * 바뀐 자리만 뽑아 적는다. 이득이 없으면 null 을 돌려준다.
106
+ *
107
+ * 줄 번호는 **새 파일의 번호**로 적는다. 모델은 그 번호로 Edit 을 걸기
108
+ * 때문에, 여기서 1부터 다시 세면 엉뚱한 줄을 고친다.
109
+ */
110
+ export function 바뀐데만(앞글, 새글) {
111
+ const 새줄들 = String(새글).split('\n');
112
+ if (새줄들.length < 최소줄) return null;
113
+
114
+ const A = 번호떼기(앞글);
115
+ const B = 번호떼기(새글);
116
+ const d = diffLines(A.몸, B.몸, { context: 3 });
117
+ if (!d.changed || d.tooBig || !d.hunks?.length) return null;
118
+
119
+ const 자리 = (n) => (n == null ? ' ' : String(n).padStart(6));
120
+ const 조각들 = [];
121
+ for (const h of d.hunks) {
122
+ const 줄들 = [];
123
+ for (let p = h.from; p <= h.to; p += 1) {
124
+ const op = d.opAt(p);
125
+ // 지운 줄에는 새 파일의 번호가 없다. 자리만 비워 두고 뺐다고만 적는다.
126
+ if (op.t === '-') 줄들.push(`-${' '.repeat(6)}\t${d.linesA[op.a] ?? ''}`);
127
+ else if (op.t === '+') 줄들.push(`+${자리(B.번호[op.b])}\t${d.linesB[op.b] ?? ''}`);
128
+ else 줄들.push(` ${자리(B.번호[op.b])}\t${d.linesB[op.b] ?? d.linesA[op.a] ?? ''}`);
129
+ }
130
+ 조각들.push(줄들.join('\n'));
131
+ }
132
+
133
+ const 몸 = 조각들.join('\n …\n');
134
+ // 이득이 없으면 통째로 싣는 편이 짧고 읽기도 쉽다.
135
+ if (몸.length >= String(새글).length * 이득선) return null;
136
+
137
+ return '앞에서 읽은 것에서 **바뀐 자리만** 적습니다'
138
+ + ` (+${d.added} −${d.removed}). 나머지는 앞에 실린 그대로입니다.\n`
139
+ + '줄 번호는 **지금 파일의 번호**입니다 — 이 번호로 고치면 됩니다.\n\n'
140
+ + 몸;
141
+ }
@@ -92,6 +92,14 @@ export class 지켜본것 {
92
92
  this.편집실패 = 0; // Edit 의 old_string 이 파일과 안 맞음
93
93
  this.되풀이 = 0; // 같은 자리를 또 부름
94
94
  this.도구성공 = 0; // 도구가 탈 없이 끝난 횟수
95
+ /*
96
+ * 다시 읽었는데 통째로 안 실은 횟수 (agent/filemem.js).
97
+ *
98
+ * 사고가 아니다 — 사고율() 에 안 들어간다. 잘한 일을 세는 자리라서
99
+ * 급을 낮추면 안 된다. 여기 두는 까닭은 화면에서 「얼마나 아꼈나」 를
100
+ * 말할 수 있어야 하기 때문이다. 안 보이면 없는 기능이나 같다.
101
+ */
102
+ this.아낀읽기 = 0;
95
103
  }
96
104
 
97
105
  걸음셈() { this.걸음++; return this; }
@@ -118,12 +126,54 @@ export class 지켜본것 {
118
126
  * @param {지켜본것} 본것 이번 대화에서 겪은 것 (없어도 된다)
119
127
  * @param {string} 사람이정한 '작음'|'보통'|'큼' — 사람이 정했으면 그게 답이다
120
128
  */
129
+ /*
130
+ * ── 급은 이름이 아니라 **연결이 알려 준 값**으로 잡는다 ─────────────────
131
+ *
132
+ * 이름에서크기() 는 `70b` 같은 파라미터 수를 찾는다. 로컬 모델은 이름에 그걸
133
+ * 적지만, 벤더 모델은 안 적는다 — `anthropic.claude-opus-4-1` 에는 B 가 없다.
134
+ * 그래서 못 읽고 '보통' 으로 떨어졌고, 실제로 Bedrock 의 Claude Opus 가
135
+ * 「한 번에 파일 6개 · 400줄 넘으면 나눠 쓰기」 를 받았다. 있는 것 중 제일 큰
136
+ * 모델에게 중급용 보조바퀴를 달아 준 셈이다.
137
+ *
138
+ * 그렇다고 `opus` `gpt-5` 같은 **이름표**를 두는 것도 답이 아니다. 그런 표는
139
+ * 새 모델이 나올 때마다 어긋나고, 어긋난 줄도 모른다. 이 저장소가 값 박힌
140
+ * 요금표를 안 두는 것과 같은 까닭이다(backend/price.js).
141
+ *
142
+ * 대신 서버가 알려 준 것을 쓴다. probeCtx() 가 켤 때 이미 받아 오는 값이다.
143
+ * 컨텍스트 길이는 급의 완벽한 잣대는 아니지만, **우리가 실제로 아는 것** 중
144
+ * 제일 가까운 잣대다. 32k 짜리에게 열 파일을 맡기면 창이 먼저 터지고,
145
+ * 200k 짜리를 세 파일에 묶어 두면 좋은 모델을 붙들어 매는 것이다.
146
+ *
147
+ * 순서가 뜻이 있다.
148
+ * 1. 파라미터 수를 **이름에서 읽었으면** 그게 제일 세다. 8B 는 창이 128k
149
+ * 여도 8B 다 — 창 크기가 머리를 키워 주지는 않는다.
150
+ * 2. 못 읽었으면 서버가 말해 준 창 크기로 잡는다.
151
+ * 3. 둘 다 모르면 '보통'.
152
+ * 그리고 이 셋 다 **실제로 돌려 본 결과**(사고율)가 언제든 덮는다.
153
+ */
154
+ const 창으로급 = [
155
+ [128000, '큼'],
156
+ [32000, '보통'],
157
+ ];
158
+
159
+ export function 첫짐작(conn) {
160
+ const 이름것 = 이름으로짐작(conn?.model);
161
+ // 이름에서 파라미터 수를 읽었으면 그대로 간다.
162
+ if (이름에서크기(conn?.model) != null) return 이름것;
163
+
164
+ const 창 = Number(conn?.ctx);
165
+ if (!Number.isFinite(창) || 창 <= 0) return 이름것;
166
+ const 찾은것 = 창으로급.find(([선]) => 창 >= 선);
167
+ const 급 = 찾은것 ? 찾은것[1] : '작음';
168
+ return { 급, 왜: `창이 ${Math.round(창 / 1000)}k 라고 서버가 알려 줍니다`, 짐작: true };
169
+ }
170
+
121
171
  export function 매김(conn, 본것 = null, 사람이정한 = null) {
122
172
  if (급들.includes(사람이정한)) {
123
173
  return { 급: 사람이정한, 왜: '사용자가 직접 정했습니다', 짐작: false };
124
174
  }
125
175
 
126
- const 첫어림 = 이름으로짐작(conn?.model);
176
+ const 첫어림 = 첫짐작(conn);
127
177
  const 율 = 본것?.사고율?.() ?? null;
128
178
  if (율 == null) return 첫어림;
129
179