deel-local-cli 2.0.0 → 2.0.3

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 (65) hide show
  1. package/README.ko.md +14 -5
  2. package/README.md +14 -5
  3. package/package.json +1 -1
  4. package/src/acp/serve.js +49 -12
  5. package/src/agent/commit.js +2 -2
  6. package/src/agent/compact.js +4 -4
  7. package/src/agent/grade.js +1 -1
  8. package/src/agent/loop.js +2 -2
  9. package/src/agent/memory.js +1 -1
  10. package/src/agent/models.js +1 -1
  11. package/src/agent/outschema.js +1 -1
  12. package/src/agent/pins.js +7 -2
  13. package/src/agent/project.js +1 -1
  14. package/src/agent/recall.js +1 -1
  15. package/src/agent/store.js +13 -26
  16. package/src/backend/ctxsize.js +2 -2
  17. package/src/backend/http.js +1 -1
  18. package/src/backend/learn.js +27 -1
  19. package/src/backend/mcp.js +97 -16
  20. package/src/backend/proxy.js +1 -1
  21. package/src/backend/quota.js +2 -2
  22. package/src/commands/common.js +2 -2
  23. package/src/commands/extend.js +0 -1
  24. package/src/commands/model.js +1 -2
  25. package/src/commands.js +4 -2
  26. package/src/completion.js +13 -3
  27. package/src/config.js +35 -13
  28. package/src/configexplain.js +3 -2
  29. package/src/doctor.js +1 -2
  30. package/src/jsonl.js +42 -0
  31. package/src/lsp/client.js +6 -46
  32. package/src/oneshot.js +22 -0
  33. package/src/pack/selfpack.js +2 -2
  34. package/src/pack/zip.js +18 -1
  35. package/src/plugins/manage.js +81 -14
  36. package/src/reap.js +70 -0
  37. package/src/report.js +1 -1
  38. package/src/safety/audit.js +4 -19
  39. package/src/safety/authcmd.js +1 -1
  40. package/src/safety/guard.js +81 -7
  41. package/src/safety/hooks.js +1 -1
  42. package/src/safety/keystore.js +18 -3
  43. package/src/safety/shellenv.js +1 -1
  44. package/src/safety/undo.js +44 -20
  45. package/src/skills/discover.js +1 -1
  46. package/src/tools/doc2md.js +25 -3
  47. package/src/tools/docs.js +3 -3
  48. package/src/tools/edit-match.js +1 -1
  49. package/src/tools/encoding.js +1 -1
  50. package/src/tools/excel-com.js +56 -30
  51. package/src/tools/fastgrep.js +65 -10
  52. package/src/tools/fsutil.js +8 -4
  53. package/src/tools/ignore.js +21 -1
  54. package/src/tools/index.js +77 -7
  55. package/src/tools/jobs.js +27 -5
  56. package/src/tools/pdf.js +1 -3
  57. package/src/tools/shell.js +27 -0
  58. package/src/tools/todo.js +1 -1
  59. package/src/tools/verify.js +17 -3
  60. package/src/tools/webfetch.js +12 -2
  61. package/src/tools/xlsx.js +4 -4
  62. package/src/ui/banner.js +1 -1
  63. package/src/ui/inputbox.js +1 -1
  64. package/src/ui/md.js +1 -1
  65. package/src/ui/motion.js +1 -1
package/README.ko.md CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  [![Node.js CI](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/test.yml?branch=main&logo=github&logoColor=white&label=Node.js%20CI)](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
22
22
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/codeql.yml?branch=main&logo=github&logoColor=white&label=CodeQL)](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
23
- [![tests](https://img.shields.io/badge/tests-13%2C900%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-14%2C151%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
24
24
 
25
25
  [![dependencies](https://img.shields.io/badge/dependencies-0-1a7f37)](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
26
26
  [![ESM](https://img.shields.io/badge/ESM-Node%2020%2B-5FA04E?logo=javascript&logoColor=white)](package.json)
@@ -243,6 +243,12 @@ Model Context Protocol 과 Agent Client Protocol 은 둘 다 표준입출력으
243
243
  npm install -g deel-local-cli
244
244
  ```
245
245
 
246
+ 이 명령은 옛 판이 깔려 있어도 늘 npm 의 최신 판을 받습니다. `npx` 로 쓸 때는 `@latest` 를 붙이세요 —
247
+ 전에 깔아 둔 deel 이 있으면 `npx deel-local-cli` 는 새로 받지 않고 **그걸** 실행합니다.
248
+ `npx deel-local-cli@latest` 는 늘 최신을 받습니다. 지금 도는 판은 `deel --version` 으로 봅니다.
249
+ `npm i -g` 로도 옛 판이 오면 `npm view deel-local-cli version` 을 보세요 — 사내 npm 미러는
250
+ npmjs.org 보다 늦을 수 있습니다.
251
+
246
252
  설치가 싫으면 소스를 받아 그대로 쓰셔도 됩니다. `npm install` 이 필요 없습니다.
247
253
 
248
254
  ```bash
@@ -316,7 +322,7 @@ deel --offline
316
322
  무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
317
323
 
318
324
  ```
319
- deel 2.0.0 ⌂ 이 안
325
+ deel 2.0.3 ⌂ 이 안
320
326
  보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
321
327
  ```
322
328
 
@@ -1240,7 +1246,7 @@ deel stats # 이 폴더에서 실제로 무엇을 했나 (.de
1240
1246
  ## 개발
1241
1247
 
1242
1248
  ```bash
1243
- npm test 전체 검증 (13,905항목 — 몇몇은 터미널에 따라 갈립니다)
1249
+ npm test 전체 검증 (14,151항목 — 몇몇은 터미널에 따라 갈립니다)
1244
1250
  npm run coverage 검사가 소스의 어디를 밟았는지
1245
1251
  npm run verify 반입·통신 검증만
1246
1252
  npm run bench 편집 성공률 측정
@@ -1297,7 +1303,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
1297
1303
  | `exitcode` · `doorparity` | 7 · 19 | 화면에 적은 종료코드 표가 진짜인가 · 문 **네 개**가 같은 것을 주는가 |
1298
1304
  | `no-bundle` | 28 | 배포 묶음에 남의 것이 안 섞였는가 · 검사 파일 위생 |
1299
1305
  | `edit-bench` | 15건 | 편집 성공률 |
1300
- | `mutate` | 어긋 1,656개 | **검사가 정말 지키는가** — 지켜야 할 줄을 일부러 어긋내고 빨개지는지 본다 |
1306
+ | `mutate` | 어긋 1,733개 | **검사가 정말 지키는가** — 지켜야 할 줄을 일부러 어긋내고 빨개지는지 본다 |
1301
1307
 
1302
1308
  > **자세히** — 어디를 밟았는지 · 폴더 구조
1303
1309
  >
@@ -1309,7 +1315,10 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
1309
1315
 
1310
1316
  | 판 | 무엇이 바뀌었나 |
1311
1317
  |---|---|
1312
- | **[2.0.0](docs/ko/releases/2.0.md#200)** | 있다고 적힌 울타리마다 실제로 걸리는지 다시 봤습니다 — 바뀐 것 전부·어디를 어떻게 고쳤나·어떻게 찾았나(CHA) |
1318
+ | **[2.0.3](docs/ko/releases/2.0.md#203)** | 2.0.2 는 npm 에 못 나갔습니다 — 리눅스에서만 틀리는 검사 둘을 고쳐 2.0.2 를 그대로 냅니다 |
1319
+ | [2.0.2](docs/ko/releases/2.0.md#202) | 2.0.1 까지 넘겨 둔 것을 다 잡았습니다 — `deel run` 에만 없던 MCP 도구, 끝난 명령의 번호로 가던 `taskkill`, 한 번 넘어지면 세션 내내 못 쓰던 MCP 서버 |
1320
+ | [2.0.1](docs/ko/releases/2.0.md#201) | 울타리 하나가 안 걸렸습니다 — `sudo -u root bash` 가 `curl … \| bash` 막이를 지나갔습니다 — 그리고 관문 넷이 빨개질 수 없었습니다 |
1321
+ | [2.0.0](docs/ko/releases/2.0.md#200) | 있다고 적힌 울타리마다 실제로 걸리는지 다시 봤습니다 — 바뀐 것 전부·어디를 어떻게 고쳤나·어떻게 찾았나(CHA) |
1313
1322
 
1314
1323
  <p align="center">
1315
1324
  <picture>
package/README.md CHANGED
@@ -20,7 +20,7 @@ Vendor APIs connect too — **only when you say so**
20
20
 
21
21
  [![Node.js CI](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/test.yml?branch=main&logo=github&logoColor=white&label=Node.js%20CI)](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
22
22
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/codeql.yml?branch=main&logo=github&logoColor=white&label=CodeQL)](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
23
- [![tests](https://img.shields.io/badge/tests-13%2C900%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-14%2C151%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
24
24
 
25
25
  [![dependencies](https://img.shields.io/badge/dependencies-0-1a7f37)](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
26
26
  [![ESM](https://img.shields.io/badge/ESM-Node%2020%2B-5FA04E?logo=javascript&logoColor=white)](package.json)
@@ -314,6 +314,12 @@ that sends `목적` instead of `purpose` still gets through.
314
314
  npm install -g deel-local-cli
315
315
  ```
316
316
 
317
+ This always installs the newest version on npm, including over an older one. With `npx`, add `@latest`:
318
+ if an older deel is already installed, `npx deel-local-cli` runs that one instead of fetching a new one —
319
+ `npx deel-local-cli@latest` always gets the newest. `deel --version` shows what you are running.
320
+ If `npm i -g` still brings an old version, check `npm view deel-local-cli version` — a company npm
321
+ mirror can lag behind npmjs.org.
322
+
317
323
  Or skip installing entirely — there is no `npm install` step:
318
324
 
319
325
  ```bash
@@ -389,7 +395,7 @@ deel --offline
389
395
  The destination is printed at the top of every session:
390
396
 
391
397
  ```
392
- deel 2.0.0 ⌂ inside
398
+ deel 2.0.3 ⌂ inside
393
399
  Sends to this machine 127.0.0.1:11434 ← nowhere else
394
400
  ```
395
401
 
@@ -1334,7 +1340,7 @@ Stored in `~/.deel/config.json`. A `.deel/config.json` in the project folder tak
1334
1340
  ## Development
1335
1341
 
1336
1342
  ```bash
1337
- npm test Full suite (13,905 checks; a few are TTY-dependent)
1343
+ npm test Full suite (14,151 checks; a few are TTY-dependent)
1338
1344
  npm run coverage Which lines the tests actually execute
1339
1345
  npm run verify Import + network checks only
1340
1346
  npm run bench Edit success rate
@@ -1378,7 +1384,7 @@ so one run tells you everything.
1378
1384
  | `exitcode` · `doorparity` | 7 · 19 | The printed exit-code table is real · all **four** doors hand out the same thing |
1379
1385
  | `no-bundle` | 28 | Nothing foreign in the published package; test-file hygiene |
1380
1386
  | `edit-bench` | 15 cases | Edit success rate |
1381
- | `mutate` | 1,656 mutants | **Whether the tests actually guard** — break the line on purpose, check it turns red |
1387
+ | `mutate` | 1,733 mutants | **Whether the tests actually guard** — break the line on purpose, check it turns red |
1382
1388
 
1383
1389
  > **More** — Coverage · Layout
1384
1390
  >
@@ -1390,7 +1396,10 @@ so one run tells you everything.
1390
1396
 
1391
1397
  | Version | What changed |
1392
1398
  |---|---|
1393
- | **[2.0.0](docs/en/releases/2.0.md#200)** | Every fence we said was there, checked for whether it actually holds — everything that changed, where and how it was fixed, and how it was found (CHA) |
1399
+ | **[2.0.3](docs/en/releases/2.0.md#203)** | 2.0.2 never reached npm — two checks that were wrong only on Linux, fixed; otherwise 2.0.2 as is |
1400
+ | [2.0.2](docs/en/releases/2.0.md#202) | Everything 2.0.1 left open, closed — MCP tools missing only from `deel run`, `taskkill` sent to an exited command's pid, MCP servers that stayed down for the session after one crash |
1401
+ | [2.0.1](docs/en/releases/2.0.md#201) | One fence in 2.0.0 did not hold — `sudo -u root bash` walked through the `curl … \| bash` guard — and four gates could not go red |
1402
+ | [2.0.0](docs/en/releases/2.0.md#200) | Every fence we said was there, checked for whether it actually holds — everything that changed, where and how it was fixed, and how it was found (CHA) |
1394
1403
 
1395
1404
  <p align="center">
1396
1405
  <picture>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.3",
4
4
  "description": "A coding-agent CLI that runs on local models and private gateways. Zero dependencies, Node 20+.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/acp/serve.js CHANGED
@@ -27,7 +27,7 @@
27
27
  // 그래서 여기서 process.stdout.write 를 통째로 바꿔 끼운다. 부르는 자리를
28
28
  // 하나하나 찾아 막는 방법도 있지만, 그건 앞으로 새로 쓰는 코드까지 계속
29
29
  // 조심해야 한다는 뜻이다 — 언젠가 반드시 한 군데를 빠뜨린다.
30
- import { existsSync, statSync } from 'node:fs';
30
+ import { existsSync, statSync, realpathSync } from 'node:fs';
31
31
  import { isAbsolute, join, resolve } from 'node:path';
32
32
  import { VERSION } from '../version.js';
33
33
  import { 규칙모으기, 늘허락, 정책읽기 } from '../safety/policy.js';
@@ -63,7 +63,7 @@ import { 연결, 줄나누기, 모르는방법오류, 잘못된인자오류, 인
63
63
  import { 도구시작, 도구끝남, 도구이름표, 도구갈래, 도구자리, 멈춘까닭, 프롬프트글, 되살린것 } from './map.js';
64
64
 
65
65
  /** 우리가 말하는 규격 판. 정수 하나이고, 깨지는 변경에서만 올라간다. */
66
- export const 규격판 = 1;
66
+ const 규격판 = 1;
67
67
 
68
68
  /**
69
69
  * 이 모드에서 표준출력을 잠근다.
@@ -74,6 +74,22 @@ export const 규격판 = 1;
74
74
  *
75
75
  * @returns {(줄: string) => void} 진짜 표준출력으로 쓰는 함수. ACP 만 이걸 쓴다
76
76
  */
77
+ /**
78
+ * 두 폴더 글이 **같은 자리**인가를 가르는 열쇠 (2.0.2 · 508).
79
+ *
80
+ * 글자로 견줬다(윈도에서만 소문자로). 그래서 한 폴더가 두 벌로 셌다 —
81
+ * 맥(대소문자를 안 가리는 APFS)의 `/Users/me/Proj` 와 `/users/me/proj`, 정션·심링크로 들어온 길,
82
+ * 8.3 짧은 이름. 같은 폴더인데 MCP 서버를 한 벌 더 띄우고, 같은 대화를 「다른 폴더로 열려
83
+ * 있다」 며 거절했다. 실제 자리(realpath)로 편 뒤에 견준다 — 맥·윈도의 realpath 는 디스크에
84
+ * 적힌 대소문자로 돌려준다. 윈도는 드라이브 글자(`C:` · `c:`)까지 맞추려고 소문자로 한 번 더 편다.
85
+ * 없는 자리면 글자 그대로 편다(견줄 것은 견준다).
86
+ */
87
+ export function 자리열쇠(p) {
88
+ let r = resolve(String(p ?? ''));
89
+ try { r = realpathSync.native(r); } catch { /* 없거나 못 읽는 자리 — 글자로 견준다 */ }
90
+ return process.platform === 'win32' ? r.toLowerCase() : r;
91
+ }
92
+
77
93
  export function 표준출력잠그기() {
78
94
  const 진짜 = process.stdout.write.bind(process.stdout);
79
95
  process.stdout.write = function (덩이, enc, cb) {
@@ -156,10 +172,7 @@ export async function acp(opts = {}) {
156
172
  * 겹쳐 온 session/new 둘을 가르려면 이름을 짓는 그 자리에서 바로 적어 둬야 한다.
157
173
  */
158
174
  const 열린자리 = new Map();
159
- const 같은자리 = (가, 나) => {
160
- const 편 = (p) => { const r = resolve(String(p)); return process.platform === 'win32' ? r.toLowerCase() : r; };
161
- return 편(가) === 편(나);
162
- };
175
+ const 같은자리 = (가, 나) => 자리열쇠(가) === 자리열쇠(나);
163
176
 
164
177
  /*
165
178
  * 폴더 → MCP 붙임. **폴더 하나에 한 벌**이다.
@@ -194,7 +207,24 @@ export async function acp(opts = {}) {
194
207
  * @param {object} o
195
208
  * @param {string|null} o.아이디 이어할 대화 이름. 주면 그 파일에 이어 쓴다
196
209
  */
197
- async function 방만들기(요청, { 아이디 = null } = {}) {
210
+ /*
211
+ * 방을 짓다 넘어지면 **잡아 둔 이름을 푼다** (2.0.2 · 508).
212
+ *
213
+ * 이름은 짓자마자 열린자리 에 적는다(겹쳐 온 session/new 가 피하게). 그런데 그 뒤에서
214
+ * 넘어지면(훅·에이전트·감사기록을 여는 자리) 방은 없는데 이름만 남아, 이 창이 끝날 때까지
215
+ * 그 이름을 「다른 폴더로 열려 있다」 며 되살리기를 거절하고 정리(prune)에서도 뺐다.
216
+ */
217
+ async function 방만들기(요청, 옵션 = {}) {
218
+ const 잡은이름 = [];
219
+ try {
220
+ return await 방짓기(요청, 옵션, 잡은이름);
221
+ } catch (err) {
222
+ for (const id of 잡은이름) if (!방들.has(id)) 열린자리.delete(id);
223
+ throw err;
224
+ }
225
+ }
226
+
227
+ async function 방짓기(요청, { 아이디 = null } = {}, 잡은이름 = []) {
198
228
  /*
199
229
  * 작업 폴더를 **설정보다 먼저** 정한다.
200
230
  *
@@ -212,9 +242,13 @@ export async function acp(opts = {}) {
212
242
  * 그 경로를 통째로 새로 만들었다. 둘 다 에디터가 잘못 준 것이니 잘못된 인자로 답한다.
213
243
  * 안 주면 예전처럼 --root(없으면 띄운 자리)를 쓴다 — 그건 사람이 띄울 때 정한 것이다.
214
244
  */
215
- const 준자리 = typeof 요청?.cwd === 'string' && 요청.cwd ? 요청.cwd : null;
245
+ /*
246
+ * **빈 글도 준 것이다** (2.0.2 · 508). 빈 글을 안 준 것으로 쳐서 --root 로 갔다 — 규격 위반 입력을
247
+ * 사람이 띄울 때 정한 자리로 조용히 바꿔 여는 것이다. 에디터가 cwd 를 못 채웠다는 뜻이니 거절한다.
248
+ */
249
+ const 준자리 = typeof 요청?.cwd === 'string' ? 요청.cwd : null;
216
250
  if (준자리 != null) {
217
- if (!isAbsolute(준자리)) throw 잘못된인자오류(`cwd 는 절대 경로여야 합니다: ${준자리}`);
251
+ if (!isAbsolute(준자리)) throw 잘못된인자오류(`cwd 는 절대 경로여야 합니다: ${준자리 || '(빈 글)'}`);
218
252
  let 폴더인가 = false;
219
253
  try { 폴더인가 = statSync(준자리).isDirectory(); } catch { /* 없다 */ }
220
254
  if (!폴더인가) throw 잘못된인자오류(`cwd 가 있는 폴더가 아닙니다: ${준자리}`);
@@ -428,6 +462,7 @@ export async function acp(opts = {}) {
428
462
  // begin() 이 이름을 옮겼을 수 있으니 그 뒤에 적는다. Store 를 지은 자리부터 여기까지
429
463
  // 기다림이 없어서, 겹쳐 온 session/new 도 이 적힌 것을 보고 이름을 피한다.
430
464
  열린자리.set(store.id, root);
465
+ 잡은이름.push(store.id);
431
466
  /*
432
467
  * 남은 할 일과 시킨 말 원문도 이 파일과 묶는다 (agent/store.js 의 살림따라가기).
433
468
  *
@@ -459,7 +494,8 @@ export async function acp(opts = {}) {
459
494
  * 그래서 deel 은 늘 제 폴더의 .deel/mcp.json 만 본다. 사람이 직접 적은 것만
460
495
  * 띄운다는 규칙이 대화 화면과 여기서 똑같이 유지된다.
461
496
  */
462
- const 캐시열쇠 = resolve(root);
497
+ // 같은 폴더면 한 벌 — 글자가 달라도 (자리열쇠 머리말 · 2.0.2 · 508). `C:` · `c:` 로 두 벌 띄웠었다.
498
+ const 캐시열쇠 = 자리열쇠(root);
463
499
  if (!mcp캐시.has(캐시열쇠)) {
464
500
  mcp캐시.set(캐시열쇠, 다붙이기(root, {
465
501
  // 봉인은 MCP 를 붙이는 자리에도 그대로 걸린다 (safety/runmode.js 의 봉인됐나).
@@ -1134,8 +1170,9 @@ export async function acp(opts = {}) {
1134
1170
  // 이름 대화를 되살리라는데 A 폴더 방이 나가서, 그 뒤 한 말이 A 에 적히고 A 를
1135
1171
  // 고친다. 폴더가 다르면 방만들기 로 보내고, 거기서 또렷하게 거절한다.
1136
1172
  const 있던방 = 방들.get(아이디);
1137
- const 바란자리 = typeof 인자?.cwd === 'string' && 인자.cwd ? 인자.cwd : (opts.root ?? process.cwd());
1138
- const 방 = 있던방 && 같은자리(있던방.root, 바란자리) ? 있던방 : await 방만들기(인자, { 아이디 });
1173
+ // 빈 cwd 는 방만들기 가 거절한다 — 여기서 --root 로 바꿔 있던 방을 내주지 않는다 (2.0.2 · 508).
1174
+ const 바란자리 = typeof 인자?.cwd === 'string' ? 인자.cwd : (opts.root ?? process.cwd());
1175
+ const 방 = 있던방 && 바란자리 && 같은자리(있던방.root, 바란자리) ? 있던방 : await 방만들기(인자, { 아이디 });
1139
1176
 
1140
1177
  /*
1141
1178
  * 화면에 그릴 것과 모델에게 줄 것이 다르다.
@@ -299,7 +299,7 @@ export function 담기(뿌리, 경로들, { 전부 = false, 안쪽 = '' } = {})
299
299
  *
300
300
  * @returns {{샌것: string[], 못뺀것: string[]}}
301
301
  */
302
- export function 살림도로빼기(뿌리, 파일들) {
302
+ function 살림도로빼기(뿌리, 파일들) {
303
303
  const 샌것 = 파일들.filter((f) => 살림에닿나(join(뿌리, f)));
304
304
  if (!샌것.length) return { 샌것, 못뺀것: [] };
305
305
  // 첫 커밋 전이면 HEAD 가 없어 restore 가 안 된다. 그때는 index 에서 지운다.
@@ -458,7 +458,7 @@ export function 답가르기(글) {
458
458
  * 글자를 감춘다), 그렇게 적힌 것은 이력에 영영 남아 남의 터미널에서 다시 돈다.
459
459
  * 줄바꿈과 탭만 남긴다.
460
460
  */
461
- export function 제어글자빼기(글) {
461
+ function 제어글자빼기(글) {
462
462
  let 남길것 = '';
463
463
  for (const 글자 of String(글 ?? '')) {
464
464
  const 값 = 글자.codePointAt(0);
@@ -14,8 +14,8 @@ import { 그림장수, 글만 } from '../backend/vision.js';
14
14
 
15
15
  // 몇 %에서 접기 시작할지. 접고 나서 다시 금방 차면 아무 소용이 없으니 넉넉히 비운다.
16
16
  export const COMPACT_AT = 0.8;
17
- export const KEEP_TAIL_RATIO = 0.3; // 최근 대화 중 남길 비율
18
- export const KEEP_HEAD = 2; // 맨 처음 요청은 언제나 남긴다 (목표가 거기 있다)
17
+ const KEEP_TAIL_RATIO = 0.3; // 최근 대화 중 남길 비율
18
+ const KEEP_HEAD = 2; // 맨 처음 요청은 언제나 남긴다 (목표가 거기 있다)
19
19
 
20
20
  /*
21
21
  * ── 그 전에, 도구 결과부터 접는다 ──────────────────────────────────────
@@ -37,7 +37,7 @@ export const KEEP_HEAD = 2; // 맨 처음 요청은 언제나 남긴
37
37
  */
38
38
  export const FOLD_AT = 0.55; // 이 아래로는 접을 이유가 없다
39
39
  export const KEEP_RECENT = 4; // 최근 도구 결과 이만큼은 원문 그대로 둔다
40
- export const FOLD_MIN = 300; // 이보다 작으면 접어도 자리가 안 준다 (글자 수)
40
+ const FOLD_MIN = 300; // 이보다 작으면 접어도 자리가 안 준다 (글자 수)
41
41
  export const 접힘표 = '(접힘)';
42
42
 
43
43
  /*
@@ -109,7 +109,7 @@ export const 캐시일때비울몫 = 0.10;
109
109
  * (여기가 「두 번 넘게」 라고 적혀 있었다. 아래 셈은 두 번째에 이미 단정한다 —
110
110
  * 한 번의 차이지만, 적힌 것과 하는 것이 다르면 다음 사람이 그 값을 못 믿는다.)
111
111
  */
112
- export const 재볼기회 = 2;
112
+ const 재볼기회 = 2;
113
113
 
114
114
  export function 캐시가도나(session) {
115
115
  const u = session?.usage ?? {};
@@ -42,7 +42,7 @@ import { 언어 } from '../i18n/index.js';
42
42
  */
43
43
 
44
44
  /** 급 이름. 숫자가 아니라 이름으로 쓰는 이유는 화면에 그대로 나가기 때문이다. */
45
- export const 급들 = ['작음', '보통', '큼'];
45
+ const 급들 = ['작음', '보통', '큼'];
46
46
 
47
47
  /**
48
48
  * 이름에서 파라미터 수를 읽어 본다.
package/src/agent/loop.js CHANGED
@@ -160,8 +160,8 @@ const 읽기전용 = new Set(['Read', 'Outline', 'Glob', 'Grep', 'Def', 'Refs',
160
160
  * 2,400토큰이고 2k 상한에서는 300토큰이다. 같은 잣대인 척하면서 실제로는
161
161
  * 상한이 클수록 헐거워진다.
162
162
  */
163
- export const 되찾을몫 = 0.4;
164
- export const 쓸모있는자리 = 1024;
163
+ const 되찾을몫 = 0.4;
164
+ const 쓸모있는자리 = 1024;
165
165
 
166
166
  /**
167
167
  * 호출 목록을 '같이 돌려도 되는 덩어리' 로 자른다.
@@ -71,7 +71,7 @@ const 머리말 = `# 기억
71
71
  * 남의 줄이 든 파일에 한 줄 **더해서** 쓸 때는 지문을 안 적는다. 적으면 남의
72
72
  * 줄이 내 줄 틈에 끼어 「이 PC 가 적은 것」 으로 세탁된다.
73
73
  */
74
- export const 내것표자리 = (env = process.env) => join(dirname(신뢰자리(env)), 'memory-own.json');
74
+ const 내것표자리 = (env = process.env) => join(dirname(신뢰자리(env)), 'memory-own.json');
75
75
  /** 지문을 들고 있는 폴더 수. 넘으면 오래 안 쓴 것부터 뺀다 — 빠진 폴더는 믿기 전까지 안 실릴 뿐이다. */
76
76
  const 내것표최대 = 200;
77
77
  /*
@@ -195,7 +195,7 @@ export function 알릴말(지금conn, 새conn) {
195
195
  /**
196
196
  * 지금 붙어 있는 프로필. `/ask` 가 '나 자신에게 묻기' 를 걸러낼 때 쓴다.
197
197
  */
198
- export function 지금프로필(cfg = load()) {
198
+ function 지금프로필(cfg = load()) {
199
199
  return activeProfile(cfg);
200
200
  }
201
201
 
@@ -210,7 +210,7 @@ function 제자리참조찾기(뿌리) {
210
210
  }
211
211
 
212
212
  /** 같은 파일 안(`#`으로 시작)이 아닌 `$ref` 를 다 찾는다. */
213
- export function 바깥ref찾기(스키마, 본것 = new Set()) {
213
+ function 바깥ref찾기(스키마, 본것 = new Set()) {
214
214
  const out = [];
215
215
  const 훑기 = (s) => {
216
216
  if (!s || typeof s !== 'object' || 본것.has(s)) return;
package/src/agent/pins.js CHANGED
@@ -43,9 +43,14 @@ export class 못박기 {
43
43
  this.것들 = this.것들.slice(0, 최대개수);
44
44
  }
45
45
 
46
+ /*
47
+ * 견줄 때는 NFC 로 맞춘다 (2.0.2 · 429). 맥에서 친 한글은 자모로 풀린 꼴(NFD)로 오기도 해서,
48
+ * 같은 「운영 DB 는 건드리지 마라」 가 두 번 박혔다 — 매 턴 실리는 자리를 같은 말이 둘 먹는다.
49
+ */
46
50
  #이미있나(말) {
47
- const 낮 = 말.toLowerCase();
48
- return this.것들.some((x) => x.toLowerCase() === 낮);
51
+ const 열쇠 = (x) => x.normalize('NFC').toLowerCase();
52
+ const 낮 = 열쇠(말);
53
+ return this.것들.some((x) => 열쇠(x) === 낮);
49
54
  }
50
55
 
51
56
  /**
@@ -68,7 +68,7 @@ const 명령이름최대 = 60;
68
68
  const 가지최대 = 120;
69
69
 
70
70
  /** 떨어져 나온 머리(detached HEAD). 135-136 주석이 적은 말과 같은 글이어야 한다. */
71
- export const 떨어진머리 = '가지 없음 (detached)';
71
+ const 떨어진머리 = '가지 없음 (detached)';
72
72
 
73
73
  /*
74
74
  * 확장자로만 갈래가 정해지는 것들 (2.0.0 8회차 판정).
@@ -23,7 +23,7 @@ import { sessionsDir } from './store.js';
23
23
  import { 부른것들, 결과들, 도구결과인가, 본문글 } from '../backend/adapter.js';
24
24
 
25
25
  // 한 번 찾을 때 읽을 최대 바이트. 넘으면 거기서 멈추고 **멈췄다고 말한다.**
26
- export const 읽기예산 = 64 * 1024 * 1024;
26
+ const 읽기예산 = 64 * 1024 * 1024;
27
27
  // 한 메시지에서 볼 최대 길이. 파일을 통째로 붙인 도구 결과가 수 MB 씩 있다.
28
28
  const 메시지최대 = 200 * 1024;
29
29
 
@@ -15,8 +15,8 @@
15
15
  import {
16
16
  existsSync, mkdirSync, readdirSync, readFileSync,
17
17
  appendFileSync, writeFileSync, statSync, rmSync, chmodSync, renameSync,
18
- openSync, readSync, closeSync,
19
18
  } from 'node:fs';
19
+ import { 줄로끝나나, 본인만잠그기 } from '../jsonl.js';
20
20
  import { join } from 'node:path';
21
21
  import { 도구결과인가 } from '../backend/adapter.js';
22
22
  import { BOM떼기 } from '../safety/trust.js';
@@ -131,9 +131,15 @@ export class Store {
131
131
  * 그래서 「안 적히고 있습니다」 가 한 번도 안 떴다. 사람이 알아차리는 자리는
132
132
  * 또 **다음 날 --resume** 이다 — 이 파일 위쪽이 너무 늦다고 적어 둔 그 자리다.
133
133
  *
134
- * 우리가 지은 이름은 freeId 와 'wx' 가 이미 막는다. 사람이 이름을 준 자리
135
- * (--resume/--continue)만 여기서 본다. 내가 적어 둔 데까지의 바이트 수를
136
- * 들고 있다가, 적기 전에 파일이 그보다 자라 있으면 남이 쓴 것이다.
134
+ * 내가 적어 둔 데까지의 바이트 수를 들고 있다가, 적기 전에 파일이 그보다 자라
135
+ * 있으면 남이 쓴 것이다.
136
+ *
137
+ * **우리가 지은 이름의 창도 본다** (2.0.2 · 527). 지은 이름은 freeId 와 'wx' 가
138
+ * 「두 창이 같은 새 이름을 집는 것」 은 막지만, 다른 창이 `--continue` 로 **이 파일을**
139
+ * 집어 오는 것은 못 막는다 — 제일 최근 대화가 바로 이것이다. 그래서 뒤에 연 창만 멈추고
140
+ * 먼저 연 창은 그 창의 줄 사이에 제 줄을 섞어 넣었고, 접으면(replace) 그 창의 줄을
141
+ * 통째로 지웠다. 이제 누가 먼저 적든 **먼저 적은 쪽이 이어 가고 다른 쪽이 멈춘다** —
142
+ * 한 파일에 두 대화가 섞이는 일이 없다. 멈춘 쪽 화면은 「안 적히고 있습니다」 를 말한다.
137
143
  *
138
144
  * 그러면 **안 적고 센다.** 그냥 이어 붙이는 것은 두 대화를 섞어 놓는 일이고,
139
145
  * 섞인 파일은 둘 다 못 쓰게 된다. 대신 잠자코 물러나지는 않는다 — 못쓴 셈에
@@ -144,7 +150,7 @@ export class Store {
144
150
  /** 지금 파일 크기. 못 재면 null — 모르는 것은 모른다고 둔다(헛경보가 더 나쁘다). */
145
151
  #크기() { try { return statSync(this.file).size; } catch { return null; } }
146
152
  #남이썼나() {
147
- if (this.auto || this.#끝자리 == null) return false;
153
+ if (this.#끝자리 == null) return false;
148
154
  const 지금 = this.#크기();
149
155
  return 지금 != null && 지금 !== this.#끝자리;
150
156
  }
@@ -167,14 +173,7 @@ export class Store {
167
173
  #잠그기() {
168
174
  if (this.#잠갔나) return;
169
175
  this.#잠갔나 = true;
170
- /*
171
- * 윈도우에서 chmod 는 **아무 일도 안 하고 성공한다.** 그 성공을 그대로
172
- * 적으면 바로 위 머리말이 스스로 금지한 「잠근 척」 이 된다 — 같은 PC 를
173
- * 여럿이 쓰는 사람이 화면만 보고 안심한다. 안 걸었으면 안 걸었다고 적는다.
174
- */
175
- if (process.platform === 'win32') { this.잠금 = { 못함: 'windows' }; return; }
176
- try { chmodSync(this.file, 0o600); this.잠금 = { 모드: 0o600 }; }
177
- catch (err) { this.잠금 = { 못함: err?.code ?? String(err) }; }
176
+ this.잠금 = 본인만잠그기(this.file);
178
177
  }
179
178
 
180
179
  #open() {
@@ -274,18 +273,6 @@ export class Store {
274
273
  * 파일마다 처음 적을 때 한 번, 끝이 개행인지 보고 아니면 개행부터 붙인다.
275
274
  */
276
275
  #줄끝봤나 = false;
277
- #줄로끝나나() {
278
- let fd = null;
279
- try {
280
- const 크기 = statSync(this.file).size;
281
- if (!크기) return true;
282
- fd = openSync(this.file, 'r');
283
- const 한바이트 = Buffer.alloc(1);
284
- readSync(fd, 한바이트, 0, 1, 크기 - 1);
285
- return 한바이트[0] === 0x0a;
286
- } catch { return true; } // 없거나 못 읽으면 붙일 반쪽도 없다
287
- finally { if (fd != null) { try { closeSync(fd); } catch { /* 닫다 터져도 적기는 한다 */ } } }
288
- }
289
276
 
290
277
  /** 한 줄 적는다. 적었으면 true. 못 적었으면 false 이고 셈에 오른다. */
291
278
  #write(obj) {
@@ -296,7 +283,7 @@ export class Store {
296
283
  if (this.#남이썼나()) { this.#못썼다({ code: 'OTHER_WINDOW' }); return false; }
297
284
  try {
298
285
  if (!this.#줄끝봤나) {
299
- if (!this.#줄로끝나나()) 줄 = '\n' + 줄;
286
+ if (!줄로끝나나(this.file)) 줄 = '\n' + 줄;
300
287
  this.#줄끝봤나 = true;
301
288
  }
302
289
  appendFileSync(this.file, 줄, 'utf8');
@@ -97,7 +97,7 @@ function 성한수(v) {
97
97
  *
98
98
  * llama.cpp 의 /props 나 일부 게이트웨이의 설명 필드도 같은 모양이다.
99
99
  */
100
- export function 글에서찾기(s, 이름들) {
100
+ function 글에서찾기(s, 이름들) {
101
101
  const 글 = String(s ?? '');
102
102
  if (!글 || 글.length > 200000) return null;
103
103
  for (const 이름 of 이름들) {
@@ -122,7 +122,7 @@ export function 글에서찾기(s, 이름들) {
122
122
  * 어느 이름에서 찾았는지도 같이 돌려준다 — 값이 이상할 때 사람이 원인을
123
123
  * 짚을 수 있어야 한다. /ctx 자세히 가 이걸 보여 준다.
124
124
  */
125
- export function 파보기(obj, 이름들, { 글도 = true, depth = 0 } = {}) {
125
+ function 파보기(obj, 이름들, { 글도 = true, depth = 0 } = {}) {
126
126
  if (!obj || typeof obj !== 'object' || depth > 5) return null;
127
127
  for (const key of 이름들) {
128
128
  const v = 성한수(obj[key]);
@@ -304,7 +304,7 @@ function 신호(timeout, signal) {
304
304
  */
305
305
  export const 잠잠기본 = 60000;
306
306
 
307
- export function 멎음오류(잠잠) {
307
+ function 멎음오류(잠잠) {
308
308
  return Object.assign(
309
309
  new Error(`${초로(잠잠)}초 동안 아무것도 안 왔습니다 — 흐름이 멎었습니다`),
310
310
  { name: 'StallError', code: 'STALL', 잠잠 },
@@ -29,6 +29,32 @@ const 성한수 = (v) => {
29
29
  return Number.isFinite(n) && n >= 최소 && n <= 최대허용 ? n : null;
30
30
  };
31
31
 
32
+ /*
33
+ * 창 한계 문장에서 **우리가 보낸 크기**를 뽑는다 (2.0.2 · 488).
34
+ *
35
+ * 한계 뒤 `requested N` 만 봤다. 요즘 vLLM 은 이렇게 말한다 —
36
+ *
37
+ * However, your request has 40000 input tokens. → 못 뽑음 (null)
38
+ * However, you requested 1024 output tokens and your prompt contains at least
39
+ * 7169 input tokens, for a total of at least 8193 tokens. → 1024 로 뽑음
40
+ *
41
+ * 뒤엣것은 틀린 수다 — 화면이 「(1,024 을 보냈었습니다)」 라고, 8,192 창에 1,024 를 보내 거절당한
42
+ * 것처럼 말한다. 합이 있으면 합, 없으면 답 길이가 아닌 `requested N`, 그다음 입력 크기 순으로 본다.
43
+ */
44
+ function 보낸크기(s) {
45
+ const 차례 = [
46
+ /for a total of\s+(?:at least\s+)?(\d+)/i,
47
+ /requested\s+(\d+)(?!\d)(?!\s*(?:output|completion|new)\b)/i,
48
+ /requested tokens\s*\(\s*(\d+)\s*\)/i,
49
+ /(?:request has|prompt contains|input (?:has|contains))\s+(?:at least\s+)?(\d+)/i,
50
+ ];
51
+ for (const re of 차례) {
52
+ const n = 성한수(re.exec(s)?.[1]);
53
+ if (n) return n;
54
+ }
55
+ return null;
56
+ }
57
+
32
58
  /**
33
59
  * 거절 문장에서 배울 것이 있나.
34
60
  *
@@ -107,7 +133,7 @@ export function 배울것(message) {
107
133
  if (!m) continue;
108
134
  const limit = 성한수(m[1]);
109
135
  if (!limit) continue;
110
- return { kind: 'ctx', limit, asked: 성한수(m[2]) ?? null, text: 짧게(원문) };
136
+ return { kind: 'ctx', limit, asked: 보낸크기(s), text: 짧게(원문) };
111
137
  }
112
138
 
113
139
  // ── 2) 답 길이 한계 ───────────────────────────────────────────────────