deel-local-cli 1.12.0 → 1.13.1

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 (51) hide show
  1. package/README.ko.md +6 -5
  2. package/README.md +6 -5
  3. package/package.json +60 -60
  4. package/src/acp/map.js +30 -25
  5. package/src/acp/serve.js +810 -810
  6. package/src/agent/budget.js +156 -156
  7. package/src/agent/commit.js +32 -9
  8. package/src/agent/compact.js +441 -420
  9. package/src/agent/effort.js +308 -308
  10. package/src/agent/evolve.js +263 -263
  11. package/src/agent/loop.js +1728 -1707
  12. package/src/agent/memory.js +156 -156
  13. package/src/agent/mention.js +210 -210
  14. package/src/agent/recall.js +22 -9
  15. package/src/agent/session.js +1056 -1049
  16. package/src/backend/adapter.js +1355 -1159
  17. package/src/backend/cachemark.js +223 -150
  18. package/src/backend/detect.js +327 -327
  19. package/src/backend/http.js +3 -1
  20. package/src/backend/mcp.js +15 -3
  21. package/src/backend/price.js +69 -6
  22. package/src/backend/probe.js +17 -3
  23. package/src/backend/quota.js +274 -250
  24. package/src/backend/tokens.js +25 -3
  25. package/src/backend/toolfit.js +352 -352
  26. package/src/backend/wire.js +715 -600
  27. package/src/commands.js +3184 -3135
  28. package/src/config.js +39 -0
  29. package/src/i18n/en.js +545 -544
  30. package/src/i18n/ja.js +498 -497
  31. package/src/i18n/ko.js +592 -591
  32. package/src/i18n/zh.js +498 -497
  33. package/src/oneshot.js +625 -620
  34. package/src/pack/tar.js +23 -1
  35. package/src/pack/zip.js +19 -1
  36. package/src/plugins/manage.js +85 -5
  37. package/src/repl.js +2616 -2602
  38. package/src/safety/audit.js +22 -3
  39. package/src/safety/guard.js +124 -9
  40. package/src/safety/network.js +43 -0
  41. package/src/tools/fastgrep.js +15 -0
  42. package/src/tools/fsutil.js +291 -265
  43. package/src/tools/index.js +40 -3
  44. package/src/tools/jobs.js +87 -6
  45. package/src/tools/label.js +52 -0
  46. package/src/tools/spawn.js +114 -0
  47. package/src/tools/verify.js +358 -358
  48. package/src/tools/webfetch.js +28 -3
  49. package/src/ui/export.js +26 -10
  50. package/src/ui/pick.js +115 -115
  51. package/src/ui/status.js +617 -613
@@ -1,263 +1,263 @@
1
- // 쓸수록 이 PC 에 맞춰 나아진다.
2
- //
3
- // ── 왜 만드나 ───────────────────────────────────────────────────────────
4
- //
5
- // deel 은 이미 세 군데서 뭔가를 배우고 있었다. 그런데 셋 다 **끄면 사라졌다.**
6
- //
7
- // backend/learn.js 서버가 거절하면 그 말에서 상한을 배운다 → 그 세션에서만
8
- // agent/grade.js 모델이 인자를 자주 잘라 먹는지 지켜본다 → 그 세션에서만
9
- // session.배운다() 토큰 추정을 실제값에 맞춘다 → 그 세션에서만
10
- //
11
- // 그래서 켤 때마다 처음부터 다시 겪는다. 어제 `pnpm` 이 이 PC 에 없다는 걸
12
- // 알아냈어도 오늘 또 부르고, 또 실패하고, 또 우회한다. 같은 실수를 매일 한다.
13
- //
14
- // 여기서는 **겪은 것을 조금씩 쌓아 두고**, 다음에 켤 때 몇 줄로 요약해 넘긴다.
15
- //
16
- // ── 학습이 아니다 ───────────────────────────────────────────────────────
17
- //
18
- // 모델을 고치는 것이 아니고, 대화를 통째로 쌓아 두는 것도 아니다. 그러면
19
- // 컨텍스트만 먹는다. 여기 쌓는 것은 **확인된 사실 몇 줄**이다.
20
- //
21
- // · 세어 두기만 한다 — 무엇이 몇 번 됐고 몇 번 안 됐나
22
- // · 확신이 설 때만 말한다 — 한 번 겪은 것은 우연일 수 있다
23
- // · 상한을 못 박는다 — 프롬프트에 실리는 것은 220토큰까지다
24
- //
25
- // 자리를 아끼는 쪽이 오히려 값이 크다. 여기 한 줄이 모델이 헛도는 걸음
26
- // 서너 개를 없앤다 — 그 걸음 하나가 파일 내용 한 벌씩이다.
27
- //
28
- // ── 무엇을 어디에 쌓나 ──────────────────────────────────────────────────
29
- //
30
- // <작업폴더>/.deel/배운것.json 이 프로젝트에서만 맞는 것 (명령·인코딩)
31
- // <설정폴더>/배운것.json 이 PC·이 모델에 붙는 것 (버릇·토큰 보정)
32
- //
33
- // 나눠 두는 이유는 하나다. "`npm test` 가 된다" 는 이 폴더의 사실이고,
34
- // "이 모델은 인자를 잘라 먹는다" 는 모델의 사실이다. 섞어 두면 폴더를 옮길
35
- // 때마다 모델에 대해 알아낸 것을 통째로 잃는다.
36
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
37
- import { join, dirname } from 'node:path';
38
- import { estimateTokens } from './session.js';
39
-
40
- export const 최대토큰 = 220; // 프롬프트에 실을 상한
41
- const 명령최대 = 40; // 표가 끝없이 자라지 않게
42
- const 모델최대 = 10;
43
-
44
- const 빈것 = () => ({ 판: 1, 명령: {}, 모델: {} });
45
-
46
- function 읽기(파일) {
47
- try {
48
- if (!existsSync(파일)) return 빈것();
49
- const j = JSON.parse(readFileSync(파일, 'utf8'));
50
- return { ...빈것(), ...(j && typeof j === 'object' ? j : {}) };
51
- } catch { return 빈것(); }
52
- }
53
-
54
- function 쓰기(파일, 값) {
55
- try {
56
- mkdirSync(dirname(파일), { recursive: true });
57
- writeFileSync(파일, JSON.stringify(값, null, 2), 'utf8');
58
- } catch { /* 못 적어도 대화는 계속돼야 한다 */ }
59
- }
60
-
61
- /** 오래된 것부터 버려서 표를 상한 안에 둔다. */
62
- function 줄이기(표, 상한) {
63
- const 열쇠 = Object.keys(표);
64
- if (열쇠.length <= 상한) return 표;
65
- 열쇠.sort((a, b) => String(표[a]?.at ?? '').localeCompare(String(표[b]?.at ?? '')));
66
- for (const k of 열쇠.slice(0, 열쇠.length - 상한)) delete 표[k];
67
- return 표;
68
- }
69
-
70
- export class 배움 {
71
- /**
72
- * @param {string} root 작업 폴더
73
- * @param {string} home 설정 폴더 (~/.deel 또는 DEEL_HOME)
74
- * @param {string} 지금 시각. 검사가 고정할 수 있게 밖에서 받는다
75
- */
76
- constructor(root, home, 지금 = new Date().toISOString()) {
77
- this.지금 = 지금;
78
- this.폴더파일 = root ? join(root, '.deel', '배운것.json') : null;
79
- this.집파일 = home ? join(home, '배운것.json') : null;
80
- this.폴더 = this.폴더파일 ? 읽기(this.폴더파일) : 빈것();
81
- this.집 = this.집파일 ? 읽기(this.집파일) : 빈것();
82
- }
83
-
84
- /**
85
- * 명령 하나가 됐는지 안 됐는지.
86
- *
87
- * 명령줄 전체가 아니라 **앞머리 두 낱말**만 센다. `npm test -- --watch` 와
88
- * `npm test` 는 같은 사실이고, 경로·인자까지 세면 표가 금세 쓸모없어진다.
89
- *
90
- * 다만 **프로그램이 아예 없어서** 실패한 것은 첫 낱말만 센다. `pnpm install`
91
- * 이 안 되고 `pnpm add` 도 안 됐다면 그건 두 가지 사실이 아니라 "이 PC 에
92
- * pnpm 이 없다" 는 한 가지 사실이다. 두 낱말로 세면 서로 다른 칸에 하나씩
93
- * 쌓여서 영영 확신에 못 이른다 — 매번 다시 겪는다.
94
- */
95
- 명령본것(명령, 됐나, 이유 = '') {
96
- const 낱말 = String(명령 ?? '').trim().split(/\s+/);
97
- const 없는프로그램 = !됐나
98
- && /not found|not recognized|ENOENT|찾을 수 없|없는 명령/i.test(String(이유 ?? ''));
99
- const 열쇠 = (없는프로그램 ? 낱말.slice(0, 1) : 낱말.slice(0, 2)).join(' ').slice(0, 40);
100
- if (!열쇠) return this;
101
- const 표 = this.폴더.명령;
102
- const r = 표[열쇠] ?? { ok: 0, no: 0, at: this.지금 };
103
- if (됐나) r.ok++; else r.no++;
104
- r.at = this.지금;
105
- 표[열쇠] = r;
106
- 줄이기(표, 명령최대);
107
- this.#저장폴더();
108
- return this;
109
- }
110
-
111
- /** 모델이 보인 버릇 하나. grade.js 의 지켜본것 과 같은 이름을 쓴다. */
112
- 모델본것(모델, 무엇, n = 1) {
113
- const r = this.#모델칸(모델);
114
- if (!r) return this;
115
- r[무엇] = (r[무엇] ?? 0) + n;
116
- r.at = this.지금;
117
- this.#저장집();
118
- return this;
119
- }
120
-
121
- /** 토큰 추정 배수. 다음에 켤 때 이 값으로 시작한다. */
122
- 보정본것(모델, 배수) {
123
- const n = Number(배수);
124
- if (!Number.isFinite(n) || n < 0.5 || n > 2) return this;
125
- const r = this.#모델칸(모델);
126
- if (!r) return this;
127
- // 갑자기 튀지 않게 지난 값과 섞는다. 대화마다 조금씩 옮겨 간다.
128
- r.보정 = r.보정 ? r.보정 + (n - r.보정) * 0.5 : n;
129
- r.at = this.지금;
130
- this.#저장집();
131
- return this;
132
- }
133
-
134
- /** 지난번에 알아낸 토큰 배수. 없으면 null. */
135
- 아는보정(모델) {
136
- const r = this.집.모델?.[String(모델 ?? '')];
137
- return r?.보정 && r.보정 >= 0.5 && r.보정 <= 2 ? r.보정 : null;
138
- }
139
-
140
- /*
141
- * ── 이 모델이 이 주소에서 받는 전선 모양 (backend/wire.js) ─────────────
142
- *
143
- * 모델 이름만으로는 못 가른다. 같은 `claude-opus-5` 라도 회사 직통과 사내
144
- * 게이트웨이가 받는 것이 다르고, 게이트웨이는 제 나름대로 깎아서 넘긴다.
145
- * 그래서 **모델과 주소를 함께** 열쇠로 쓴다.
146
- *
147
- * 주소는 host 만 쓴다. 경로에는 배포 이름·판 번호 같은 것이 붙어 있고,
148
- * 그건 사람이 설정을 조금만 바꿔도 달라져서 배운 것이 매번 새것이 된다.
149
- */
150
- #전선열쇠(모델, 주소, 규격) {
151
- let host = '';
152
- try { host = new URL(String(주소 ?? '')).host; } catch { host = ''; }
153
- /*
154
- * 규격도 열쇠에 넣는다.
155
- *
156
- * 경로를 뺐더니 **한 호스트에 창구가 둘인 자리**가 걸렸다. mantle 이
157
- * 그렇다 — `/openai/v1` 과 `/anthropic/v1` 이 같은 호스트에 같은 모델
158
- * 이름으로 서 있다(providers/bedrock.js). 규격을 안 넣으면 OpenAI 창구에서
159
- * 배운 것(`생각형식:'effort'` 따위)이 Anthropic 창구 카드 위에 얹히고,
160
- * 그러면 그 창구에서는 생각도 캐시 표식도 조용히 다 꺼진다.
161
- *
162
- * 경로 전체를 넣지 않는 까닭은 그대로다 — 배포 이름·판 번호가 붙어 있어서
163
- * 사람이 설정을 조금만 바꿔도 배운 것이 매번 새것이 된다. 규격은 그런
164
- * 이름이 아니라 **몸을 어떻게 짓는가** 라서, 달라지면 정말 다른 카드다.
165
- */
166
- const 꼴 = String(규격 ?? '').trim();
167
- return `${String(모델 ?? '').trim()}@${host}${꼴 ? `#${꼴}` : ''}`;
168
- }
169
-
170
- /** 전선 모양 하나를 적어 둔다. */
171
- 전선본것(모델, 주소, 카드, 규격) {
172
- if (!카드) return this;
173
- const 열쇠 = this.#전선열쇠(모델, 주소, 규격);
174
- if (열쇠 === '@') return this;
175
- this.집.전선 = this.집.전선 ?? {};
176
- this.집.전선[열쇠] = { ...카드, at: this.지금 };
177
- 줄이기(this.집.전선, 모델최대);
178
- this.#저장집();
179
- return this;
180
- }
181
-
182
- /** 지난번에 알아낸 전선 모양. 없으면 null. */
183
- 아는전선(모델, 주소, 규격) {
184
- const r = this.집.전선?.[this.#전선열쇠(모델, 주소, 규격)];
185
- if (!r || typeof r !== 'object') return null;
186
- const { at: _때, ...나머지 } = r;
187
- return 나머지;
188
- }
189
-
190
- #모델칸(모델) {
191
- const 이름 = String(모델 ?? '').trim();
192
- if (!이름) return null;
193
- const 표 = this.집.모델;
194
- 표[이름] = 표[이름] ?? { 걸음: 0, at: this.지금 };
195
- 줄이기(표, 모델최대);
196
- return 표[이름];
197
- }
198
-
199
- #저장폴더() { if (this.폴더파일) 쓰기(this.폴더파일, this.폴더); }
200
- #저장집() { if (this.집파일) 쓰기(this.집파일, this.집); }
201
-
202
- /**
203
- * 프롬프트에 실을 몇 줄.
204
- *
205
- * 확신이 서는 것만 말한다. 두 번 겪지 않은 것은 우연일 수 있고, 우연을
206
- * 사실처럼 적으면 모델이 그 위에서 엉뚱한 판단을 한다 — 안 배우느니만 못하다.
207
- *
208
- * @returns {string|null} 없으면 null (그때는 프롬프트에 아무것도 안 붙는다)
209
- */
210
- 요약(모델 = '', 상한 = 최대토큰) {
211
- const 줄 = [];
212
-
213
- // 되는 명령 / 안 되는 명령. 이 폴더의 사실이다.
214
- const 되는것 = [];
215
- const 안되는것 = [];
216
- for (const [열쇠, r] of Object.entries(this.폴더.명령 ?? {})) {
217
- if (r.ok >= 2 && r.no === 0) 되는것.push(열쇠);
218
- else if (r.no >= 2 && r.ok === 0) 안되는것.push(열쇠);
219
- }
220
- if (되는것.length) 줄.push(`- 여기서 되는 명령: ${되는것.slice(0, 6).map((x) => `\`${x}\``).join(' · ')}`);
221
- if (안되는것.length) 줄.push(`- 이 PC 에서 안 되는 명령(다시 부르지 마라): ${안되는것.slice(0, 6).map((x) => `\`${x}\``).join(' · ')}`);
222
-
223
- // 모델 버릇. 걸음이 쌓여야 말이 된다.
224
- const m = this.집.모델?.[String(모델 ?? '')];
225
- if (m && (m.걸음 ?? 0) >= 10) {
226
- if ((m.잘린인자 ?? 0) / m.걸음 >= 0.15) {
227
- 줄.push('- 이 모델은 큰 인자를 자주 잘라 먹었다. 파일은 Write 한 번에 다 쓰지 말고 Append 로 나눠 써라.');
228
- }
229
- if ((m.편집실패 ?? 0) / m.걸음 >= 0.15) {
230
- 줄.push('- Edit 이 자주 빗나갔다. old_string 은 Read 로 본 그대로, 공백까지 옮겨 담아라.');
231
- }
232
- if ((m.되풀이 ?? 0) / m.걸음 >= 0.15) {
233
- 줄.push('- 같은 호출을 되풀이한 적이 잦다. 한 번 실패한 방법은 바꿔서 시도해라.');
234
- }
235
- }
236
-
237
- if (!줄.length) return null;
238
-
239
- const 머리 = '## 여기서 겪어 본 것 (자동으로 쌓임)';
240
- const out = [머리];
241
- for (const l of 줄) {
242
- if (estimateTokens([...out, l].join('\n')) > 상한) break;
243
- out.push(l);
244
- }
245
- return out.length > 1 ? out.join('\n') : null;
246
- }
247
-
248
- /** 사람이 지우라고 한 것. 파일째 비운다. */
249
- 지우기(어디 = '전부') {
250
- if (어디 === '전부' || 어디 === '폴더') { this.폴더 = 빈것(); this.#저장폴더(); }
251
- if (어디 === '전부' || 어디 === '모델') { this.집 = 빈것(); this.#저장집(); }
252
- return this;
253
- }
254
-
255
- /** 화면에 뿌릴 것. */
256
- 현황(모델 = '') {
257
- const 명령 = Object.entries(this.폴더.명령 ?? {})
258
- .map(([이름, r]) => ({ 이름, ...r }))
259
- .sort((a, b) => (b.ok + b.no) - (a.ok + a.no));
260
- const m = this.집.모델?.[String(모델 ?? '')] ?? null;
261
- return { 명령, 모델: m, 모델이름: String(모델 ?? '') };
262
- }
263
- }
1
+ // 쓸수록 이 PC 에 맞춰 나아진다.
2
+ //
3
+ // ── 왜 만드나 ───────────────────────────────────────────────────────────
4
+ //
5
+ // deel 은 이미 세 군데서 뭔가를 배우고 있었다. 그런데 셋 다 **끄면 사라졌다.**
6
+ //
7
+ // backend/learn.js 서버가 거절하면 그 말에서 상한을 배운다 → 그 세션에서만
8
+ // agent/grade.js 모델이 인자를 자주 잘라 먹는지 지켜본다 → 그 세션에서만
9
+ // session.배운다() 토큰 추정을 실제값에 맞춘다 → 그 세션에서만
10
+ //
11
+ // 그래서 켤 때마다 처음부터 다시 겪는다. 어제 `pnpm` 이 이 PC 에 없다는 걸
12
+ // 알아냈어도 오늘 또 부르고, 또 실패하고, 또 우회한다. 같은 실수를 매일 한다.
13
+ //
14
+ // 여기서는 **겪은 것을 조금씩 쌓아 두고**, 다음에 켤 때 몇 줄로 요약해 넘긴다.
15
+ //
16
+ // ── 학습이 아니다 ───────────────────────────────────────────────────────
17
+ //
18
+ // 모델을 고치는 것이 아니고, 대화를 통째로 쌓아 두는 것도 아니다. 그러면
19
+ // 컨텍스트만 먹는다. 여기 쌓는 것은 **확인된 사실 몇 줄**이다.
20
+ //
21
+ // · 세어 두기만 한다 — 무엇이 몇 번 됐고 몇 번 안 됐나
22
+ // · 확신이 설 때만 말한다 — 한 번 겪은 것은 우연일 수 있다
23
+ // · 상한을 못 박는다 — 프롬프트에 실리는 것은 220토큰까지다
24
+ //
25
+ // 자리를 아끼는 쪽이 오히려 값이 크다. 여기 한 줄이 모델이 헛도는 걸음
26
+ // 서너 개를 없앤다 — 그 걸음 하나가 파일 내용 한 벌씩이다.
27
+ //
28
+ // ── 무엇을 어디에 쌓나 ──────────────────────────────────────────────────
29
+ //
30
+ // <작업폴더>/.deel/배운것.json 이 프로젝트에서만 맞는 것 (명령·인코딩)
31
+ // <설정폴더>/배운것.json 이 PC·이 모델에 붙는 것 (버릇·토큰 보정)
32
+ //
33
+ // 나눠 두는 이유는 하나다. "`npm test` 가 된다" 는 이 폴더의 사실이고,
34
+ // "이 모델은 인자를 잘라 먹는다" 는 모델의 사실이다. 섞어 두면 폴더를 옮길
35
+ // 때마다 모델에 대해 알아낸 것을 통째로 잃는다.
36
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
37
+ import { join, dirname } from 'node:path';
38
+ import { estimateTokens } from './session.js';
39
+
40
+ export const 최대토큰 = 220; // 프롬프트에 실을 상한
41
+ const 명령최대 = 40; // 표가 끝없이 자라지 않게
42
+ const 모델최대 = 10;
43
+
44
+ const 빈것 = () => ({ 판: 1, 명령: {}, 모델: {} });
45
+
46
+ function 읽기(파일) {
47
+ try {
48
+ if (!existsSync(파일)) return 빈것();
49
+ const j = JSON.parse(readFileSync(파일, 'utf8'));
50
+ return { ...빈것(), ...(j && typeof j === 'object' ? j : {}) };
51
+ } catch { return 빈것(); }
52
+ }
53
+
54
+ function 쓰기(파일, 값) {
55
+ try {
56
+ mkdirSync(dirname(파일), { recursive: true });
57
+ writeFileSync(파일, JSON.stringify(값, null, 2), 'utf8');
58
+ } catch { /* 못 적어도 대화는 계속돼야 한다 */ }
59
+ }
60
+
61
+ /** 오래된 것부터 버려서 표를 상한 안에 둔다. */
62
+ function 줄이기(표, 상한) {
63
+ const 열쇠 = Object.keys(표);
64
+ if (열쇠.length <= 상한) return 표;
65
+ 열쇠.sort((a, b) => String(표[a]?.at ?? '').localeCompare(String(표[b]?.at ?? '')));
66
+ for (const k of 열쇠.slice(0, 열쇠.length - 상한)) delete 표[k];
67
+ return 표;
68
+ }
69
+
70
+ export class 배움 {
71
+ /**
72
+ * @param {string} root 작업 폴더
73
+ * @param {string} home 설정 폴더 (~/.deel 또는 DEEL_HOME)
74
+ * @param {string} 지금 시각. 검사가 고정할 수 있게 밖에서 받는다
75
+ */
76
+ constructor(root, home, 지금 = new Date().toISOString()) {
77
+ this.지금 = 지금;
78
+ this.폴더파일 = root ? join(root, '.deel', '배운것.json') : null;
79
+ this.집파일 = home ? join(home, '배운것.json') : null;
80
+ this.폴더 = this.폴더파일 ? 읽기(this.폴더파일) : 빈것();
81
+ this.집 = this.집파일 ? 읽기(this.집파일) : 빈것();
82
+ }
83
+
84
+ /**
85
+ * 명령 하나가 됐는지 안 됐는지.
86
+ *
87
+ * 명령줄 전체가 아니라 **앞머리 두 낱말**만 센다. `npm test -- --watch` 와
88
+ * `npm test` 는 같은 사실이고, 경로·인자까지 세면 표가 금세 쓸모없어진다.
89
+ *
90
+ * 다만 **프로그램이 아예 없어서** 실패한 것은 첫 낱말만 센다. `pnpm install`
91
+ * 이 안 되고 `pnpm add` 도 안 됐다면 그건 두 가지 사실이 아니라 "이 PC 에
92
+ * pnpm 이 없다" 는 한 가지 사실이다. 두 낱말로 세면 서로 다른 칸에 하나씩
93
+ * 쌓여서 영영 확신에 못 이른다 — 매번 다시 겪는다.
94
+ */
95
+ 명령본것(명령, 됐나, 이유 = '') {
96
+ const 낱말 = String(명령 ?? '').trim().split(/\s+/);
97
+ const 없는프로그램 = !됐나
98
+ && /not found|not recognized|ENOENT|찾을 수 없|없는 명령/i.test(String(이유 ?? ''));
99
+ const 열쇠 = (없는프로그램 ? 낱말.slice(0, 1) : 낱말.slice(0, 2)).join(' ').slice(0, 40);
100
+ if (!열쇠) return this;
101
+ const 표 = this.폴더.명령;
102
+ const r = 표[열쇠] ?? { ok: 0, no: 0, at: this.지금 };
103
+ if (됐나) r.ok++; else r.no++;
104
+ r.at = this.지금;
105
+ 표[열쇠] = r;
106
+ 줄이기(표, 명령최대);
107
+ this.#저장폴더();
108
+ return this;
109
+ }
110
+
111
+ /** 모델이 보인 버릇 하나. grade.js 의 지켜본것 과 같은 이름을 쓴다. */
112
+ 모델본것(모델, 무엇, n = 1) {
113
+ const r = this.#모델칸(모델);
114
+ if (!r) return this;
115
+ r[무엇] = (r[무엇] ?? 0) + n;
116
+ r.at = this.지금;
117
+ this.#저장집();
118
+ return this;
119
+ }
120
+
121
+ /** 토큰 추정 배수. 다음에 켤 때 이 값으로 시작한다. */
122
+ 보정본것(모델, 배수) {
123
+ const n = Number(배수);
124
+ if (!Number.isFinite(n) || n < 0.5 || n > 2) return this;
125
+ const r = this.#모델칸(모델);
126
+ if (!r) return this;
127
+ // 갑자기 튀지 않게 지난 값과 섞는다. 대화마다 조금씩 옮겨 간다.
128
+ r.보정 = r.보정 ? r.보정 + (n - r.보정) * 0.5 : n;
129
+ r.at = this.지금;
130
+ this.#저장집();
131
+ return this;
132
+ }
133
+
134
+ /** 지난번에 알아낸 토큰 배수. 없으면 null. */
135
+ 아는보정(모델) {
136
+ const r = this.집.모델?.[String(모델 ?? '')];
137
+ return r?.보정 && r.보정 >= 0.5 && r.보정 <= 2 ? r.보정 : null;
138
+ }
139
+
140
+ /*
141
+ * ── 이 모델이 이 주소에서 받는 전선 모양 (backend/wire.js) ─────────────
142
+ *
143
+ * 모델 이름만으로는 못 가른다. 같은 `claude-opus-5` 라도 회사 직통과 사내
144
+ * 게이트웨이가 받는 것이 다르고, 게이트웨이는 제 나름대로 깎아서 넘긴다.
145
+ * 그래서 **모델과 주소를 함께** 열쇠로 쓴다.
146
+ *
147
+ * 주소는 host 만 쓴다. 경로에는 배포 이름·판 번호 같은 것이 붙어 있고,
148
+ * 그건 사람이 설정을 조금만 바꿔도 달라져서 배운 것이 매번 새것이 된다.
149
+ */
150
+ #전선열쇠(모델, 주소, 규격) {
151
+ let host = '';
152
+ try { host = new URL(String(주소 ?? '')).host; } catch { host = ''; }
153
+ /*
154
+ * 규격도 열쇠에 넣는다.
155
+ *
156
+ * 경로를 뺐더니 **한 호스트에 창구가 둘인 자리**가 걸렸다. mantle 이
157
+ * 그렇다 — `/openai/v1` 과 `/anthropic/v1` 이 같은 호스트에 같은 모델
158
+ * 이름으로 서 있다(providers/bedrock.js). 규격을 안 넣으면 OpenAI 창구에서
159
+ * 배운 것(`생각형식:'effort'` 따위)이 Anthropic 창구 카드 위에 얹히고,
160
+ * 그러면 그 창구에서는 생각도 캐시 표식도 조용히 다 꺼진다.
161
+ *
162
+ * 경로 전체를 넣지 않는 까닭은 그대로다 — 배포 이름·판 번호가 붙어 있어서
163
+ * 사람이 설정을 조금만 바꿔도 배운 것이 매번 새것이 된다. 규격은 그런
164
+ * 이름이 아니라 **몸을 어떻게 짓는가** 라서, 달라지면 정말 다른 카드다.
165
+ */
166
+ const 꼴 = String(규격 ?? '').trim();
167
+ return `${String(모델 ?? '').trim()}@${host}${꼴 ? `#${꼴}` : ''}`;
168
+ }
169
+
170
+ /** 전선 모양 하나를 적어 둔다. */
171
+ 전선본것(모델, 주소, 카드, 규격) {
172
+ if (!카드) return this;
173
+ const 열쇠 = this.#전선열쇠(모델, 주소, 규격);
174
+ if (열쇠 === '@') return this;
175
+ this.집.전선 = this.집.전선 ?? {};
176
+ this.집.전선[열쇠] = { ...카드, at: this.지금 };
177
+ 줄이기(this.집.전선, 모델최대);
178
+ this.#저장집();
179
+ return this;
180
+ }
181
+
182
+ /** 지난번에 알아낸 전선 모양. 없으면 null. */
183
+ 아는전선(모델, 주소, 규격) {
184
+ const r = this.집.전선?.[this.#전선열쇠(모델, 주소, 규격)];
185
+ if (!r || typeof r !== 'object') return null;
186
+ const { at: _때, ...나머지 } = r;
187
+ return 나머지;
188
+ }
189
+
190
+ #모델칸(모델) {
191
+ const 이름 = String(모델 ?? '').trim();
192
+ if (!이름) return null;
193
+ const 표 = this.집.모델;
194
+ 표[이름] = 표[이름] ?? { 걸음: 0, at: this.지금 };
195
+ 줄이기(표, 모델최대);
196
+ return 표[이름];
197
+ }
198
+
199
+ #저장폴더() { if (this.폴더파일) 쓰기(this.폴더파일, this.폴더); }
200
+ #저장집() { if (this.집파일) 쓰기(this.집파일, this.집); }
201
+
202
+ /**
203
+ * 프롬프트에 실을 몇 줄.
204
+ *
205
+ * 확신이 서는 것만 말한다. 두 번 겪지 않은 것은 우연일 수 있고, 우연을
206
+ * 사실처럼 적으면 모델이 그 위에서 엉뚱한 판단을 한다 — 안 배우느니만 못하다.
207
+ *
208
+ * @returns {string|null} 없으면 null (그때는 프롬프트에 아무것도 안 붙는다)
209
+ */
210
+ 요약(모델 = '', 상한 = 최대토큰) {
211
+ const 줄 = [];
212
+
213
+ // 되는 명령 / 안 되는 명령. 이 폴더의 사실이다.
214
+ const 되는것 = [];
215
+ const 안되는것 = [];
216
+ for (const [열쇠, r] of Object.entries(this.폴더.명령 ?? {})) {
217
+ if (r.ok >= 2 && r.no === 0) 되는것.push(열쇠);
218
+ else if (r.no >= 2 && r.ok === 0) 안되는것.push(열쇠);
219
+ }
220
+ if (되는것.length) 줄.push(`- 여기서 되는 명령: ${되는것.slice(0, 6).map((x) => `\`${x}\``).join(' · ')}`);
221
+ if (안되는것.length) 줄.push(`- 이 PC 에서 안 되는 명령(다시 부르지 마라): ${안되는것.slice(0, 6).map((x) => `\`${x}\``).join(' · ')}`);
222
+
223
+ // 모델 버릇. 걸음이 쌓여야 말이 된다.
224
+ const m = this.집.모델?.[String(모델 ?? '')];
225
+ if (m && (m.걸음 ?? 0) >= 10) {
226
+ if ((m.잘린인자 ?? 0) / m.걸음 >= 0.15) {
227
+ 줄.push('- 이 모델은 큰 인자를 자주 잘라 먹었다. 파일은 Write 한 번에 다 쓰지 말고 Append 로 나눠 써라.');
228
+ }
229
+ if ((m.편집실패 ?? 0) / m.걸음 >= 0.15) {
230
+ 줄.push('- Edit 이 자주 빗나갔다. old_string 은 Read 로 본 그대로, 공백까지 옮겨 담아라.');
231
+ }
232
+ if ((m.되풀이 ?? 0) / m.걸음 >= 0.15) {
233
+ 줄.push('- 같은 호출을 되풀이한 적이 잦다. 한 번 실패한 방법은 바꿔서 시도해라.');
234
+ }
235
+ }
236
+
237
+ if (!줄.length) return null;
238
+
239
+ const 머리 = '## 여기서 겪어 본 것 (자동으로 쌓임)';
240
+ const out = [머리];
241
+ for (const l of 줄) {
242
+ if (estimateTokens([...out, l].join('\n')) > 상한) break;
243
+ out.push(l);
244
+ }
245
+ return out.length > 1 ? out.join('\n') : null;
246
+ }
247
+
248
+ /** 사람이 지우라고 한 것. 파일째 비운다. */
249
+ 지우기(어디 = '전부') {
250
+ if (어디 === '전부' || 어디 === '폴더') { this.폴더 = 빈것(); this.#저장폴더(); }
251
+ if (어디 === '전부' || 어디 === '모델') { this.집 = 빈것(); this.#저장집(); }
252
+ return this;
253
+ }
254
+
255
+ /** 화면에 뿌릴 것. */
256
+ 현황(모델 = '') {
257
+ const 명령 = Object.entries(this.폴더.명령 ?? {})
258
+ .map(([이름, r]) => ({ 이름, ...r }))
259
+ .sort((a, b) => (b.ok + b.no) - (a.ok + a.no));
260
+ const m = this.집.모델?.[String(모델 ?? '')] ?? null;
261
+ return { 명령, 모델: m, 모델이름: String(모델 ?? '') };
262
+ }
263
+ }