deel-local-cli 1.14.0 → 1.15.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.
- package/README.ko.md +10 -9
- package/README.md +10 -9
- package/package.json +1 -1
- package/src/acp/serve.js +50 -7
- package/src/agent/compact.js +25 -2
- package/src/agent/filemem.js +17 -0
- package/src/agent/session.js +36 -1
- package/src/agent/threads.js +33 -0
- package/src/commands.js +48 -2
- package/src/lsp/servers.js +9 -3
- package/src/repl.js +1 -1
- package/src/safety/undo.js +37 -2
- package/src/tools/fsutil.js +36 -1
- package/src/tools/index.js +129 -25
- package/src/tools/lsp.js +1 -1
- package/src/tools/outline.js +2 -2
- package/src/tools/verify.js +2 -2
package/README.ko.md
CHANGED
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
| [속도와 씀씀이](docs/ko/tuning.md) | 단계별 추론 강도 · 프리픽스 캐시 · 컨텍스트 길이 |
|
|
95
95
|
| [안전망과 사내 반입](docs/ko/safety.md) | 되돌리기 · 작업 범위 · 감사기록 · 심사 서류 |
|
|
96
96
|
| [설정](docs/ko/config.md) · [개발](docs/ko/develop.md) | 환경변수 · 실행 옵션 · 검사 돌리기 · 폴더 구조 |
|
|
97
|
-
| [릴리스 노트](docs/ko/releases.md) | [1.14.x](docs/ko/releases/1.14.md) · [1.13.x](docs/ko/releases/1.13.md) · [1.12.x](docs/ko/releases/1.12.md) · [1.10.x](docs/ko/releases/1.10.md) · [1.9.x](docs/ko/releases/1.9.md) · [그 앞](docs/ko/releases.md) |
|
|
97
|
+
| [릴리스 노트](docs/ko/releases.md) | [1.15.x](docs/ko/releases/1.15.md) · [1.14.x](docs/ko/releases/1.14.md) · [1.13.x](docs/ko/releases/1.13.md) · [1.12.x](docs/ko/releases/1.12.md) · [1.10.x](docs/ko/releases/1.10.md) · [1.9.x](docs/ko/releases/1.9.md) · [그 앞](docs/ko/releases.md) |
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
@@ -267,7 +267,7 @@ deel --offline
|
|
|
267
267
|
무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
|
|
268
268
|
|
|
269
269
|
```
|
|
270
|
-
deel 1.
|
|
270
|
+
deel 1.15.0 ⌂ 이 안
|
|
271
271
|
보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
|
|
272
272
|
```
|
|
273
273
|
|
|
@@ -1087,7 +1087,7 @@ deel sbom --only sbom # SBOM 한 장만
|
|
|
1087
1087
|
## 개발
|
|
1088
1088
|
|
|
1089
1089
|
```bash
|
|
1090
|
-
npm test 전체 검증 (7,
|
|
1090
|
+
npm test 전체 검증 (7,144항목 — 몇몇은 터미널에 따라 갈립니다)
|
|
1091
1091
|
npm run coverage 검사가 소스의 어디를 밟았는지
|
|
1092
1092
|
npm run verify 반입·통신 검증만
|
|
1093
1093
|
npm run bench 편집 성공률 측정
|
|
@@ -1133,18 +1133,18 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1133
1133
|
| `detect` | 85 | 주소 한 줄로 규격·인증을 짚어내는가 |
|
|
1134
1134
|
| `modes` · `route` | 120 · 72 | 작업 모드 · 종합에서 알맞은 모드로 옮겨 가는가 |
|
|
1135
1135
|
| `ctxsize` | 56 | 모델에 걸린 컨텍스트 길이를 긁어오는가 |
|
|
1136
|
-
| `commands` · `commands-more` |
|
|
1136
|
+
| `commands` · `commands-more` | 221 · 93 | 슬래시 명령 전부 |
|
|
1137
1137
|
| `ui` · `ui2` | 89 · 40 | 암호 가림·한글 폭·상태줄·대화 목록·엑셀→글 |
|
|
1138
|
-
| `encoding` · `xlsx` |
|
|
1139
|
-
| `compact` |
|
|
1140
|
-
| `stuck` · `undo` | 48 ·
|
|
1138
|
+
| `encoding` · `xlsx` | 77 · 72 | 한글 인코딩 판별 · 엑셀 읽기 |
|
|
1139
|
+
| `compact` | 101 | 요약 압축·짝 안 깨짐·실패 시 물러섬 |
|
|
1140
|
+
| `stuck` · `undo` | 48 · 58 | 헛도는 것과 일이 되는 것을 가르는가 · 셸이 쓴 파일도 되돌려지는가 |
|
|
1141
1141
|
| `store` | 75 | 대화 저장·이어하기·중간에 죽어도 복구 |
|
|
1142
1142
|
| `scan` | 30 | 여러 런타임을 구분해 찾는가 |
|
|
1143
1143
|
| `plugins` | 79 | 플러그인 받기·묶기·ZIP/TAR |
|
|
1144
|
-
| `exitcode` · `doorparity` | 7 · 7 | 화면에 적은 종료코드 표가 진짜인가 · 문
|
|
1144
|
+
| `exitcode` · `doorparity` | 7 · 7 | 화면에 적은 종료코드 표가 진짜인가 · 문 **네 개**가 같은 것을 주는가 |
|
|
1145
1145
|
| `no-bundle` | 21 | 배포 묶음에 남의 것이 안 섞였는가 · 검사 파일 위생 |
|
|
1146
1146
|
| `edit-bench` | 20건 | 편집 성공률 |
|
|
1147
|
-
| `mutate` | 어긋
|
|
1147
|
+
| `mutate` | 어긋 10개 | **검사가 정말 지키는가** — 지켜야 할 줄을 일부러 어긋내고 빨개지는지 본다 |
|
|
1148
1148
|
|
|
1149
1149
|
> **자세히** — 어디를 밟았는지 · 폴더 구조
|
|
1150
1150
|
>
|
|
@@ -1156,6 +1156,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1156
1156
|
|
|
1157
1157
|
| 판 | 무엇이 바뀌었나 |
|
|
1158
1158
|
|---|---|
|
|
1159
|
+
| **[1.15.0](docs/ko/releases/1.15.md#1150)** | 같은 것을 두 번 하지 않습니다 — 프리픽스 캐시를 깨던 조용히 비싼 아홉 자리 |
|
|
1159
1160
|
| **[1.14.0](docs/ko/releases/1.14.md#1140)** | 적어 둔 것이 참인지 기계가 대신 봅니다 — 초록불과 「깨면 잡힌다」 는 다릅니다 |
|
|
1160
1161
|
| [1.13.1](docs/ko/releases/1.13.md#1131) | 올라간 물건이 소스와 달랐습니다 — 148개 중 47개가 CRLF 로 나갔습니다 |
|
|
1161
1162
|
| **[1.13.0](docs/ko/releases/1.13.md#1130)** | 바깥 창구와 붙는 자리를 전부 다시 봤습니다 — 서로 못 보는 검수자 셋, 셋이 같이 짚은 것 넷 |
|
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ This page is the **summary**. Each section links to the detail behind it.
|
|
|
119
119
|
| [Speed and spend](docs/en/tuning.md) | Per-stage effort · the prefix cache · context length |
|
|
120
120
|
| [Safety and corporate review](docs/en/safety.md) | Undo · working scope · audit log · the review package |
|
|
121
121
|
| [Configuration](docs/en/config.md) · [Development](docs/en/develop.md) | Env vars · run flags · running the tests · folder layout |
|
|
122
|
-
| [Release notes](docs/en/releases.md) | [1.14.x](docs/en/releases/1.14.md) · [1.13.x](docs/en/releases/1.13.md) · [1.12.x](docs/en/releases/1.12.md) · [1.10.x](docs/en/releases/1.10.md) · [1.9.x](docs/en/releases/1.9.md) · [older](docs/en/releases.md) |
|
|
122
|
+
| [Release notes](docs/en/releases.md) | [1.15.x](docs/en/releases/1.15.md) · [1.14.x](docs/en/releases/1.14.md) · [1.13.x](docs/en/releases/1.13.md) · [1.12.x](docs/en/releases/1.12.md) · [1.10.x](docs/en/releases/1.10.md) · [1.9.x](docs/en/releases/1.9.md) · [older](docs/en/releases.md) |
|
|
123
123
|
|
|
124
124
|
---
|
|
125
125
|
|
|
@@ -338,7 +338,7 @@ deel --offline
|
|
|
338
338
|
The destination is printed at the top of every session:
|
|
339
339
|
|
|
340
340
|
```
|
|
341
|
-
deel 1.
|
|
341
|
+
deel 1.15.0 ⌂ inside
|
|
342
342
|
Sends to this machine 127.0.0.1:11434 ← nowhere else
|
|
343
343
|
```
|
|
344
344
|
|
|
@@ -1158,7 +1158,7 @@ Stored in `~/.deel/config.json`. A `.deel/config.json` in the project folder tak
|
|
|
1158
1158
|
## Development
|
|
1159
1159
|
|
|
1160
1160
|
```bash
|
|
1161
|
-
npm test Full suite (7,
|
|
1161
|
+
npm test Full suite (7,144 checks; a few are TTY-dependent)
|
|
1162
1162
|
npm run coverage Which lines the tests actually execute
|
|
1163
1163
|
npm run verify Import + network checks only
|
|
1164
1164
|
npm run bench Edit success rate
|
|
@@ -1190,18 +1190,18 @@ so one run tells you everything.
|
|
|
1190
1190
|
| `detect` | 85 | Identifying shape and auth from one address |
|
|
1191
1191
|
| `modes` · `route` | 120 · 72 | Work modes; auto-switching from Auto |
|
|
1192
1192
|
| `ctxsize` | 56 | Reading context length off the model |
|
|
1193
|
-
| `commands` · `commands-more` |
|
|
1193
|
+
| `commands` · `commands-more` | 221 · 93 | Every slash command |
|
|
1194
1194
|
| `ui` · `ui2` | 89 · 40 | Password masking, CJK width, status line, session list, Excel→text |
|
|
1195
|
-
| `encoding` · `xlsx` |
|
|
1196
|
-
| `compact` |
|
|
1197
|
-
| `stuck` · `undo` | 48 ·
|
|
1195
|
+
| `encoding` · `xlsx` | 77 · 72 | Legacy-encoding detection; Excel reading |
|
|
1196
|
+
| `compact` | 101 | Summary folding, pairing intact, graceful fallback |
|
|
1197
|
+
| `stuck` · `undo` | 48 · 58 | Telling spinning apart from real progress · shell-written files are undoable too |
|
|
1198
1198
|
| `store` | 75 | Session persistence, resume, crash recovery |
|
|
1199
1199
|
| `scan` | 30 | Distinguishing multiple runtimes |
|
|
1200
1200
|
| `plugins` | 79 | Plugin fetch/pack, ZIP/TAR |
|
|
1201
|
-
| `exitcode` · `doorparity` | 7 · 7 | The printed exit-code table is real · all
|
|
1201
|
+
| `exitcode` · `doorparity` | 7 · 7 | The printed exit-code table is real · all **four** doors hand out the same thing |
|
|
1202
1202
|
| `no-bundle` | 21 | Nothing foreign in the published package; test-file hygiene |
|
|
1203
1203
|
| `edit-bench` | 20 cases | Edit success rate |
|
|
1204
|
-
| `mutate` |
|
|
1204
|
+
| `mutate` | 10 mutants | **Whether the tests actually guard** — break the line on purpose, check it turns red |
|
|
1205
1205
|
|
|
1206
1206
|
> **More** — Coverage · Layout
|
|
1207
1207
|
>
|
|
@@ -1213,6 +1213,7 @@ so one run tells you everything.
|
|
|
1213
1213
|
|
|
1214
1214
|
| Version | What changed |
|
|
1215
1215
|
|---|---|
|
|
1216
|
+
| **[1.15.0](docs/en/releases/1.15.md#1150)** | Stop doing the same thing twice — nine quietly expensive places that broke the prefix cache |
|
|
1216
1217
|
| **[1.14.0](docs/en/releases/1.14.md#1140)** | A machine now checks that what we wrote down is true — green does not mean guarded |
|
|
1217
1218
|
| [1.13.1](docs/en/releases/1.13.md#1131) | What shipped was not what the source said — 47 of 148 files went out with CRLF line endings |
|
|
1218
1219
|
| **[1.13.0](docs/en/releases/1.13.md#1130)** | Every place deel touches someone else's endpoint, re-examined — three independent reviewers, four findings they shared |
|
package/package.json
CHANGED
package/src/acp/serve.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// 하나하나 찾아 막는 방법도 있지만, 그건 앞으로 새로 쓰는 코드까지 계속
|
|
29
29
|
// 조심해야 한다는 뜻이다 — 언젠가 반드시 한 군데를 빠뜨린다.
|
|
30
30
|
import { existsSync } from 'node:fs';
|
|
31
|
-
import { join } from 'node:path';
|
|
31
|
+
import { join, resolve } from 'node:path';
|
|
32
32
|
import { VERSION } from '../version.js';
|
|
33
33
|
import { 규칙모으기, 늘허락, 정책읽기 } from '../safety/policy.js';
|
|
34
34
|
import { 받기설정 } from '../safety/authcmd.js';
|
|
@@ -89,6 +89,26 @@ export async function acp(opts = {}) {
|
|
|
89
89
|
|
|
90
90
|
/** 세션 하나. 에디터의 탭 하나에 해당한다. */
|
|
91
91
|
const 방들 = new Map();
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* 폴더 → MCP 붙임. **폴더 하나에 한 벌**이다.
|
|
95
|
+
*
|
|
96
|
+
* 에디터에서 `session/new` 는 탭 하나다. 사람은 한 프로젝트를 열어 놓고 탭을
|
|
97
|
+
* 서너 개 띄운다 — 그게 에디터를 쓰는 이유다. 그런데 방마다 다붙이기() 를
|
|
98
|
+
* 새로 부르면 같은 `.deel/mcp.json` 을 보고 같은 서버를 탭 수만큼 띄운다.
|
|
99
|
+
*
|
|
100
|
+
* MCP 서버는 남의 프로그램이라 무엇을 들고 뜨는지 우리는 모른다 — 인덱스를
|
|
101
|
+
* 통째로 메모리에 올리는 것도 있고, 뜰 때마다 원격에 붙는 것도 있다. 탭
|
|
102
|
+
* 하나 여는 값도 두 번 치른다: 붙기는 왕복 두 번(initialize + tools/list)이고
|
|
103
|
+
* 그동안 그 탭은 멈춰 있다.
|
|
104
|
+
*
|
|
105
|
+
* 값으로 **약속(Promise)** 을 넣는다. 에디터가 탭 셋을 한꺼번에 열면
|
|
106
|
+
* session/new 셋이 겹쳐 도는데, 다 붙은 뒤에 넣으면 셋 다 빈 칸을 보고
|
|
107
|
+
* 셋 다 띄운다 — 고치려던 것이 그대로 남는다.
|
|
108
|
+
*
|
|
109
|
+
* 폴더가 다르면 따로 띄운다. 옆 프로젝트의 도구가 딸려 오면 안 된다.
|
|
110
|
+
*/
|
|
111
|
+
const mcp캐시 = new Map();
|
|
92
112
|
let 클라이언트 = null; // initialize 로 받은 저쪽 소개
|
|
93
113
|
let 시작했나 = false;
|
|
94
114
|
|
|
@@ -235,10 +255,29 @@ export async function acp(opts = {}) {
|
|
|
235
255
|
* 그래서 deel 은 늘 제 폴더의 .deel/mcp.json 만 본다. 사람이 직접 적은 것만
|
|
236
256
|
* 띄운다는 규칙이 대화 화면과 여기서 똑같이 유지된다.
|
|
237
257
|
*/
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
258
|
+
const 캐시열쇠 = resolve(root);
|
|
259
|
+
if (!mcp캐시.has(캐시열쇠)) {
|
|
260
|
+
mcp캐시.set(캐시열쇠, 다붙이기(root, {
|
|
261
|
+
offline: !!(opts.offline ?? prof.offline),
|
|
262
|
+
audit: null,
|
|
263
|
+
}).then((붙임) => {
|
|
264
|
+
/*
|
|
265
|
+
* 못 붙은 것은 **못 붙었다고 말한다.**
|
|
266
|
+
*
|
|
267
|
+
* 대화 화면은 이걸 적어 준다(repl.js 의 경고 줄). 여기만 안 적고
|
|
268
|
+
* 있었다. 그러면 에디터 안에서는 도구가 그냥 없다 — 왜 없는지는
|
|
269
|
+
* 어디에도 안 남는다. 사람은 모델이 게을러진 줄 안다.
|
|
270
|
+
*/
|
|
271
|
+
for (const m of 붙임.못한것 ?? []) 로그(`MCP ${m.이름} 을 못 붙였습니다 — ${m.왜}`);
|
|
272
|
+
if (붙임.서버들.length) 로그(`MCP ${붙임.서버들.length}대에 붙었습니다 (${캐시열쇠}) — 이 폴더의 다른 탭도 같이 씁니다.`);
|
|
273
|
+
return 붙임;
|
|
274
|
+
}).catch((err) => {
|
|
275
|
+
// 붙이다 터져도 탭은 열려야 한다. 도구가 없는 채로 대화는 된다.
|
|
276
|
+
로그(`MCP 를 붙이다 실패했습니다 — ${err?.message ?? err}`);
|
|
277
|
+
return { 서버들: [], 못한것: [], 설정: null };
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
const mcp붙임 = await mcp캐시.get(캐시열쇠);
|
|
242
281
|
session.mcp = mcp붙임.서버들;
|
|
243
282
|
if (Array.isArray(요청?.mcpServers) && 요청.mcpServers.length) {
|
|
244
283
|
로그(`에디터가 MCP 서버 ${요청.mcpServers.length}대를 넘겼지만 띄우지 않습니다 — deel 은 .deel/mcp.json 에 적힌 것만 띄웁니다.`);
|
|
@@ -818,8 +857,12 @@ export async function acp(opts = {}) {
|
|
|
818
857
|
*/
|
|
819
858
|
const 껐다 = 일감모두끝내기();
|
|
820
859
|
if (껐다) 로그(`뒤에서 돌던 명령 ${껐다}개를 껐습니다.`);
|
|
821
|
-
|
|
822
|
-
|
|
860
|
+
// 방이 아니라 **캐시**를 돈다. 방 여럿이 한 벌을 나눠 쓰므로 방을 돌면
|
|
861
|
+
// 같은 서버에 닫기() 를 여러 번 부르게 된다.
|
|
862
|
+
for (const 약속 of mcp캐시.values()) {
|
|
863
|
+
let 붙임 = null;
|
|
864
|
+
try { 붙임 = await 약속; } catch { /* 붙다 만 것은 닫을 것도 없다 */ }
|
|
865
|
+
for (const s of 붙임?.서버들 ?? []) { try { s.닫기(); } catch { /* 이미 죽은 것 */ } }
|
|
823
866
|
}
|
|
824
867
|
if (!시작했나) 로그('initialize 를 못 받고 끝났습니다 — 이 명령은 에디터가 자식 프로세스로 띄우는 자리입니다.');
|
|
825
868
|
return 0;
|
package/src/agent/compact.js
CHANGED
|
@@ -184,10 +184,33 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN,
|
|
|
184
184
|
* 한 줄 아끼자고 앞머리를 깨면 그 걸음에 대화가 통째로 다시 나간다.
|
|
185
185
|
* 모아 두었다가 한 번에 접는 편이 눈에 띄게 싸다. 자리가 정말 모자라면
|
|
186
186
|
* 80% 에서 요약 압축이 받아 준다 — 안전망은 그쪽에 있다.
|
|
187
|
+
*
|
|
188
|
+
* ── 토큰만 세는 것으로는 묶음이 안 된다 ──────────────────────────────
|
|
189
|
+
*
|
|
190
|
+
* 이 머리말은 「접기는 묶음으로 한다」 고 적어 두고, 걸음마다 하나씩 접던
|
|
191
|
+
* 것을 고쳤다고 말한다. 그런데 막는 자가 이득문턱(2,000토큰) 하나뿐이라
|
|
192
|
+
* **평범한 Read 결과 하나가 혼자서 그 문턱을 넘는다.** 60줄짜리 결과 하나가
|
|
193
|
+
* 3천 토큰이다. 그래서 55% 를 넘긴 뒤로는 실제로 **걸음마다 한 개씩** 접혔다
|
|
194
|
+
* — 고쳤다고 적어 둔 바로 그 모양 그대로. 재 보니 접을 만한 20걸음에서
|
|
195
|
+
* 20번 접었다.
|
|
196
|
+
*
|
|
197
|
+
* 값이 어떻게 되나: 한 걸음에 3천 토큰 아끼려고, 접힌 자리 뒤 전부(1만
|
|
198
|
+
* 2천 토큰쯤)를 캐시 읽기가 아니라 정가 입력으로 다시 보낸다. 그걸 걸음마다
|
|
199
|
+
* 한다. 로컬이면 그 걸음마다 프리필을 통째로 다시 돈다.
|
|
200
|
+
*
|
|
201
|
+
* 그래서 **개수도 같이 본다.** 최근 keep 개는 어차피 남기므로, 새로 밀려난
|
|
202
|
+
* 것이 keep 개 모일 때까지 기다렸다가 한 번에 접는다. 접는 횟수가 1/keep 로
|
|
203
|
+
* 줄고, 한 번 깰 때 아끼는 양은 그만큼 커진다 — 깨는 값은 같은데.
|
|
204
|
+
*
|
|
205
|
+
* 이미 접힌 것은 여기 안 셈해진다(위 filter 의 min 이 걸러낸다 — 접힌 자국은
|
|
206
|
+
* 100자도 안 된다). 그래서 이 수는 언제나 **새로 접을 것**의 수다.
|
|
187
207
|
*/
|
|
188
208
|
const 예상이득 = 접을것.reduce((a, x) => a + estimateTokens(x.글), 0);
|
|
189
|
-
if (예상이득 < 이득문턱) {
|
|
190
|
-
return {
|
|
209
|
+
if (예상이득 < 이득문턱 || 접을것.length < keep) {
|
|
210
|
+
return {
|
|
211
|
+
접은것: 0, 아낀토큰: 0, 접은것들: [], 미룸: true,
|
|
212
|
+
모인것: 예상이득, 모인개수: 접을것.length,
|
|
213
|
+
};
|
|
191
214
|
}
|
|
192
215
|
|
|
193
216
|
let 아낀토큰 = 0;
|
package/src/agent/filemem.js
CHANGED
|
@@ -49,6 +49,23 @@ export class 파일기억 {
|
|
|
49
49
|
this.것들 = new Map();
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 이 기억을 그대로 베낀 새것.
|
|
54
|
+
*
|
|
55
|
+
* 갈래를 **갈라낼 때**(agent/threads.js 의 갈라내기) 쓴다. 그때 대화는
|
|
56
|
+
* 사본이 되는데 기억을 같이 쓰면, 한쪽에서 파일을 다시 읽은 것이 다른 쪽의
|
|
57
|
+
* 답을 바꾼다 — 갈라낸 쪽에서 v3 을 읽으면 본줄기가 「안 바뀌었다」 고
|
|
58
|
+
* 답하기 시작한다. 본줄기 대화에 실려 있는 것은 v1 인데도.
|
|
59
|
+
*
|
|
60
|
+
* 얕은 사본이면 된다. 담긴 값 { 글, 언제 } 는 갈아 끼우기만 하고 안에서
|
|
61
|
+
* 고치는 자리가 없다(실을것 은 언제나 set 으로 통째로 덮는다).
|
|
62
|
+
*/
|
|
63
|
+
베끼기() {
|
|
64
|
+
const 새것 = new 파일기억();
|
|
65
|
+
새것.것들 = new Map(this.것들);
|
|
66
|
+
return 새것;
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
/**
|
|
53
70
|
* 요약해서 접었다 = 앞엣것이 대화에서 사라졌다. 기억도 같이 버린다.
|
|
54
71
|
*
|
package/src/agent/session.js
CHANGED
|
@@ -382,8 +382,41 @@ export class Session {
|
|
|
382
382
|
return this.routed ?? this.work;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
/*
|
|
386
|
+
* ── 급은 **턴마다 한 번**만 매긴다 ──────────────────────────────────────
|
|
387
|
+
*
|
|
388
|
+
* 이 값은 시스템 글의 **굳은 부분**에 들어간다(아래 시스템조각 의 급말).
|
|
389
|
+
* 그런데 매김() 은 살아 있는 사고 카운터를 본다 — 인자 잘림 한 번, 편집
|
|
390
|
+
* 실패 한 번이면 **걸음 사이에** '보통' → '작음' 으로 뒤집힌다.
|
|
391
|
+
*
|
|
392
|
+
* 뒤집히는 자리가 프롬프트 맨 앞이라 그 뒤 전부가 캐시에서 빠진다 — 도구
|
|
393
|
+
* 정의 + 시스템 글 + 대화 전체. 그것도 턴 사이가 아니라 **한 턴의 걸음
|
|
394
|
+
* 사이**에서다. 대화가 꼬리만 자라서 앞머리가 가장 안정적이어야 할 바로
|
|
395
|
+
* 그 자리다. 게이트웨이면 캐시 읽기(0.1배)로 나갔을 것이 정가로 다시 나가고,
|
|
396
|
+
* 로컬이면 그 한 걸음에 프리필을 통째로 다시 돈다. 화면에는 왜 갑자기
|
|
397
|
+
* 느려졌는지 한 줄도 안 뜬다.
|
|
398
|
+
*
|
|
399
|
+
* 그래서 빗장을 건다. 새로 배운 것을 버리는 것이 아니라 **적용을 다음 턴으로
|
|
400
|
+
* 미루는 것**이다 — 턴시작() 이 빗장을 푼다. 사람이 /grade 로 정하거나
|
|
401
|
+
* /model 로 갈아타면 급다시재기() 로 그 자리에서 다시 맨다. 그때는 사람이
|
|
402
|
+
* 프롬프트가 바뀐 것을 알고 한 일이다.
|
|
403
|
+
*/
|
|
404
|
+
#급빗장 = null;
|
|
405
|
+
|
|
385
406
|
/** 지금 매겨진 모델 급. 화면과 프롬프트가 같은 것을 봐야 한다. */
|
|
386
|
-
급() {
|
|
407
|
+
급() {
|
|
408
|
+
if (!this.#급빗장) this.#급빗장 = 매김(this.conn, this.본것, this.급정한것);
|
|
409
|
+
return this.#급빗장;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* 빗장을 풀어 다음에 물을 때 다시 매기게 한다.
|
|
414
|
+
*
|
|
415
|
+
* 부르는 자리는 셋이다 — 턴 시작(아래 턴시작), `/grade`, `/model`
|
|
416
|
+
* (commands.js 의 연결적용). 앞의 둘은 사람이 그러라고 한 것이고, 마지막은
|
|
417
|
+
* 모델 자체가 바뀐 것이라 앞의 매김이 뜻을 잃는다.
|
|
418
|
+
*/
|
|
419
|
+
급다시재기() { this.#급빗장 = null; }
|
|
387
420
|
|
|
388
421
|
/** 이 급에서 쓸 손잡이 값들 (한 번에 만들 파일 수 같은 것). */
|
|
389
422
|
급값() { return 급값(this.급().급); }
|
|
@@ -553,6 +586,8 @@ export class Session {
|
|
|
553
586
|
/** 새 턴을 연다. 바로 다음에 push 되는 말이 이 턴의 첫 말이 된다. */
|
|
554
587
|
턴시작(턴) {
|
|
555
588
|
if (턴 == null) return this;
|
|
589
|
+
// 급은 턴마다 한 번 매긴다 — 까닭은 급() 머리말에.
|
|
590
|
+
this.급다시재기();
|
|
556
591
|
// 접혀 없어진 표는 여기서 턴다 — 안 그러면 긴 대화에서 끝없이 쌓인다.
|
|
557
592
|
this.#턴표 = this.#턴표.filter((x) => this.messages.includes(x.표));
|
|
558
593
|
if (this.#턴표.length > Session.#표최대) this.#턴표 = this.#턴표.slice(-Session.#표최대);
|
package/src/agent/threads.js
CHANGED
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
// 한 줄기로 읽어서, 서로 상관없는 대화가 한 덩어리가 된다.
|
|
28
28
|
|
|
29
29
|
/** 이름이 없을 때 붙여 줄 이름. 사람이 목록에서 알아볼 수 있으면 된다. */
|
|
30
|
+
import { 파일기억 } from './filemem.js';
|
|
31
|
+
|
|
30
32
|
function 기본이름(n) { return `갈래 ${n}`; }
|
|
31
33
|
|
|
32
34
|
export class Threads {
|
|
@@ -49,9 +51,20 @@ export class Threads {
|
|
|
49
51
|
* 돌아오는데 남은 할 일은 안 돌아왔다.
|
|
50
52
|
*/
|
|
51
53
|
첫store?.살림따라가기?.(session);
|
|
54
|
+
/*
|
|
55
|
+
* 파일기억·filesRead 도 **갈래 것**이다.
|
|
56
|
+
*
|
|
57
|
+
* 이 둘은 「messages 안에 무엇이 살아 있나」 의 그림자다. messages 를
|
|
58
|
+
* 갈래마다 따로 두면서 이것만 세션에 하나로 두면 반드시 어긋나고, 어긋나면
|
|
59
|
+
* 「앞에서 읽은 그대로입니다」 가 **그 글이 없는 갈래에서** 나온다 —
|
|
60
|
+
* 모델은 대화 어디에도 없는 글을 가리키는 쪽지를 받고, 다시 읽어도 파일이
|
|
61
|
+
* 안 바뀌었으니 같은 쪽지를 받는다. 세 번째에 헛돈다고 턴이 죽는다.
|
|
62
|
+
* (agent/filemem.js 머리말의 「통째로 다시 싣는 것보다 훨씬 나쁘다」)
|
|
63
|
+
*/
|
|
52
64
|
this.갈래들 = [{
|
|
53
65
|
이름: '본줄기', messages: session.messages, usage: session.usage, todos: ctx?.todos ?? null,
|
|
54
66
|
할일: session.할일, 이번요청: session.이번요청, store: 첫store,
|
|
67
|
+
파일기억: session.파일기억, filesRead: session.filesRead,
|
|
55
68
|
}];
|
|
56
69
|
this.자리 = 0;
|
|
57
70
|
this.센것 = 1;
|
|
@@ -72,6 +85,9 @@ export class Threads {
|
|
|
72
85
|
// 적어 버린다 — 이어받을 때 하지도 않은 일이 본줄기에 남는다.
|
|
73
86
|
g.할일 = this.session.할일;
|
|
74
87
|
g.이번요청 = this.session.이번요청;
|
|
88
|
+
// 읽은 파일 기억도 같이 — 까닭은 생성자에 적어 뒀다.
|
|
89
|
+
g.파일기억 = this.session.파일기억;
|
|
90
|
+
g.filesRead = this.session.filesRead;
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
/** 갈래 하나를 화면으로 꺼낸다. */
|
|
@@ -83,6 +99,15 @@ export class Threads {
|
|
|
83
99
|
if (this.ctx) this.ctx.todos = g.todos;
|
|
84
100
|
this.session.할일 = g.할일 ?? [];
|
|
85
101
|
this.session.이번요청 = g.이번요청 ?? '';
|
|
102
|
+
/*
|
|
103
|
+
* 여기서 session.파일기억 을 통째로 갈아 끼운다.
|
|
104
|
+
*
|
|
105
|
+
* 이미 잊기() 를 부르는 자리들(compact.js · loop.js · session.js)은 전부
|
|
106
|
+
* session.파일기억 을 거치므로 손댈 것이 없다 — 그때그때 지금 갈래의 것을
|
|
107
|
+
* 잊는다. 문이 하나로 남는다.
|
|
108
|
+
*/
|
|
109
|
+
if (g.파일기억) this.session.파일기억 = g.파일기억;
|
|
110
|
+
if (g.filesRead) this.session.filesRead = g.filesRead;
|
|
86
111
|
// 적는 자리도 지금 갈래의 파일로 옮긴다 (agent/store.js 의 살림따라가기).
|
|
87
112
|
g.store?.살림따라가기?.(this.session);
|
|
88
113
|
// 상태줄이 지금 어느 갈래인지 보여 줄 수 있게 남긴다.
|
|
@@ -110,6 +135,14 @@ export class Threads {
|
|
|
110
135
|
할일: [],
|
|
111
136
|
이번요청: '',
|
|
112
137
|
store,
|
|
138
|
+
/*
|
|
139
|
+
* 갈라낸 것이면 지금 기억을 **베껴** 물려준다. 같이 쓰면 한쪽에서 파일을
|
|
140
|
+
* 다시 읽은 것이 다른 쪽 답을 바꾼다 — 갈라낸 쪽에서 v3 을 읽으면
|
|
141
|
+
* 본줄기가 「안 바뀌었다」 고 답하기 시작한다. 본줄기에 실린 것은 v1 인데도.
|
|
142
|
+
* 빈 갈래면 빈 기억이다. 대화가 비었으니 기억할 것도 없다.
|
|
143
|
+
*/
|
|
144
|
+
파일기억: 물려줄것 ? (this.session.파일기억?.베끼기?.() ?? new 파일기억()) : new 파일기억(),
|
|
145
|
+
filesRead: 물려줄것 ? new Map(this.session.filesRead ?? []) : new Map(),
|
|
113
146
|
};
|
|
114
147
|
this.갈래들.push(g);
|
|
115
148
|
// 갈라낸 것은 지금까지 오간 말을 새 파일에도 적어 둔다. 안 적으면 그 갈래를
|
package/src/commands.js
CHANGED
|
@@ -13,7 +13,9 @@ import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js
|
|
|
13
13
|
import { 주소가리기 } from './safety/secrets.js';
|
|
14
14
|
import { pick, confirm } from './ui/prompt.js';
|
|
15
15
|
import { load, save, 저장시도, resolveKey, upsert, 열쇠보관, configPath } from './config.js';
|
|
16
|
-
import { 지금상태 as
|
|
16
|
+
import { 지금상태 as 지금열쇠상태, 받기설정, 잊기 as 받은열쇠잊기 } from './safety/authcmd.js';
|
|
17
|
+
// 열쇠받기 명령을 정책이 못박아 뒀을 수 있다 — 받기설정 이 그 값을 같이 본다.
|
|
18
|
+
import { 정책읽기 } from './safety/policy.js';
|
|
17
19
|
import { 제공자고르기 } from './providers/index.js';
|
|
18
20
|
import { 종, 알릴만한초 } from './ui/notify.js';
|
|
19
21
|
/*
|
|
@@ -431,7 +433,7 @@ export async function handle(line, session, ctx) {
|
|
|
431
433
|
}
|
|
432
434
|
|
|
433
435
|
const { 있는것, 없는것 } = 둘러보기();
|
|
434
|
-
const 뿌리것 = ctx?.scope?.root ? 프로젝트갈래(ctx.scope.root) : null;
|
|
436
|
+
const 뿌리것 = ctx?.scope?.root ? await 프로젝트갈래(ctx.scope.root) : null;
|
|
435
437
|
const 떠있는것 = 지금것들();
|
|
436
438
|
say('');
|
|
437
439
|
if (있는것.length) {
|
|
@@ -2616,6 +2618,9 @@ function 모델급(session, arg = '') {
|
|
|
2616
2618
|
|
|
2617
2619
|
if (값 === 'auto' || 값 === '자동') {
|
|
2618
2620
|
session.급정한것 = null;
|
|
2621
|
+
// 급은 턴마다 한 번만 맨다(session.js 의 급 머리말). 사람이 여기서 정한
|
|
2622
|
+
// 것은 다음 턴이 아니라 **지금** 먹어야 하니 빗장을 풀어 준다.
|
|
2623
|
+
session.급다시재기?.();
|
|
2619
2624
|
const g = session.급();
|
|
2620
2625
|
say('');
|
|
2621
2626
|
say(` ${c.cyan('◈')} 모델 급을 다시 ${c.bold('스스로 잡게')} 했습니다 — 지금은 ${c.white(g.급)}`);
|
|
@@ -2625,6 +2630,7 @@ function 모델급(session, arg = '') {
|
|
|
2625
2630
|
|
|
2626
2631
|
if (값 && 별명[값]) {
|
|
2627
2632
|
session.급정한것 = 별명[값];
|
|
2633
|
+
session.급다시재기?.();
|
|
2628
2634
|
const v = session.급값();
|
|
2629
2635
|
say('');
|
|
2630
2636
|
say(` ${c.cyan('◈')} 모델 급을 ${c.bold(별명[값])} 으로 정했습니다.`);
|
|
@@ -2785,7 +2791,47 @@ function 연결적용(session, p, ctx = null) {
|
|
|
2785
2791
|
kind: p.kind, base: p.baseUrl, auth: p.auth, key: resolveKey(p), model: p.model,
|
|
2786
2792
|
ctx: p.ctx, maxTokens: p.maxTokens ?? null,
|
|
2787
2793
|
streaming: p.streaming, tools: p.tools, json: p.json, think: p.think,
|
|
2794
|
+
/*
|
|
2795
|
+
* ── 이 두 줄이 없어서 회사 토큰이 남의 창구로 나갔다 ────────────────
|
|
2796
|
+
*
|
|
2797
|
+
* conn 을 짓는 자리는 넷이다 — repl.js · oneshot.js · acp/serve.js, 그리고
|
|
2798
|
+
* 여기. 앞의 셋은 이 둘을 넣는데 여기만 빠져 있었다. 그래서 `/model` 로
|
|
2799
|
+
* 갈아타면 **옛 프로필의 것이 그대로 남았다.**
|
|
2800
|
+
*
|
|
2801
|
+
* 회사 프로필(열쇠받기로 SSO 토큰을 받아 온다) → /model 남의창구
|
|
2802
|
+
* · 살아 있는 회사 토큰이 `Authorization` 에 실려 남의 host 로 간다
|
|
2803
|
+
* · 새 프로필의 제 열쇠(conn.key)는 **쓰이지도 않는다** —
|
|
2804
|
+
* adapter.js 의 머리말짓기 는 열쇠받기가 있으면 그쪽을 쓴다
|
|
2805
|
+
* · 남의 창구는 그 토큰을 모르니 401, 그러면 로그인 명령을 다시 돌린다
|
|
2806
|
+
*
|
|
2807
|
+
* 반대쪽도 같다. 맨 프로필에서 회사 프로필로 옮기면 열쇠받기가 null 로
|
|
2808
|
+
* 남아 401 뒤에 열쇠를 다시 받는 길이 잠긴다(adapter.js 의 열쇠다시받을까).
|
|
2809
|
+
*
|
|
2810
|
+
* vision 도 같은 값이다 — 안 옮기면 그림을 못 보는 모델에 그림을 보낸다.
|
|
2811
|
+
*/
|
|
2812
|
+
열쇠받기: 받기설정(p, { 정책값: 정책읽기().값 }),
|
|
2813
|
+
vision: p.vision ?? false,
|
|
2788
2814
|
});
|
|
2815
|
+
/*
|
|
2816
|
+
* 들고 있던 토큰도 버린다.
|
|
2817
|
+
*
|
|
2818
|
+
* 열쇠받기 설정을 새것으로 갈아도, authcmd.js 가 **모듈 전역 하나**에
|
|
2819
|
+
* 받은 토큰을 들고 있다(받은것). 어느 프로필 것인지는 안 적혀 있다. 그래서
|
|
2820
|
+
* 안 버리면 새 창구의 첫 요청에 옛 회사 토큰이 그대로 실려 나간다 — 위에서
|
|
2821
|
+
* 고친 것을 캐시가 도로 되돌리는 셈이다.
|
|
2822
|
+
*
|
|
2823
|
+
* 이 함수의 머리말이 「검사와 `/model` 갈아타기가 부른다」 라고 적혀 있었는데
|
|
2824
|
+
* 부르는 곳이 검사밖에 없었다. 이제 적힌 대로가 된다.
|
|
2825
|
+
*/
|
|
2826
|
+
받은열쇠잊기();
|
|
2827
|
+
/*
|
|
2828
|
+
* 급도 다시 맨다.
|
|
2829
|
+
*
|
|
2830
|
+
* 급은 conn 을 보고 매긴다(agent/grade.js 의 매김). 모델을 갈아탔는데 앞
|
|
2831
|
+
* 모델로 매긴 급을 턴 끝까지 들고 있으면, 화면에 적힌 급과 프롬프트에 실린
|
|
2832
|
+
* 급이 서로 다른 모델 이야기가 된다.
|
|
2833
|
+
*/
|
|
2834
|
+
session.급다시재기?.();
|
|
2789
2835
|
// 자물쇠도 같이 옮긴다. 이걸 빼먹으면 옛 주소가 열린 채로 남고 새 주소는 막혀
|
|
2790
2836
|
// 다음 한마디에서 바로 "허용되지 않은 주소" 가 난다.
|
|
2791
2837
|
allowEndpoint(p.baseUrl);
|
package/src/lsp/servers.js
CHANGED
|
@@ -193,13 +193,17 @@ export function 둘러보기(env = process.env) {
|
|
|
193
193
|
*
|
|
194
194
|
* 뿌리마다 한 번만 센다. 폴더 훑기는 값이 있고, 세션 중에 언어가 바뀌지 않는다.
|
|
195
195
|
*/
|
|
196
|
-
export function 프로젝트갈래(뿌리, env = process.env) {
|
|
196
|
+
export async function 프로젝트갈래(뿌리, env = process.env) {
|
|
197
|
+
// 센 것이 아니라 **세는 약속**을 담아 둔다. 훑기가 비동기라, 다 세고 나서
|
|
198
|
+
// 담으면 겹쳐 부른 쪽이 둘 다 빈 칸을 보고 둘 다 훑는다 — 에디터에서 탭을
|
|
199
|
+
// 한꺼번에 열면 실제로 그렇게 된다.
|
|
197
200
|
if (_센것.has(뿌리)) return _센것.get(뿌리);
|
|
201
|
+
const 약속 = (async () => {
|
|
198
202
|
let 답 = null;
|
|
199
203
|
try {
|
|
200
204
|
const 셈 = new Map();
|
|
201
205
|
const 첫파일 = new Map();
|
|
202
|
-
for (const f of walk(뿌리, { limit: 4000 })) {
|
|
206
|
+
for (const f of await walk(뿌리, { limit: 4000 })) {
|
|
203
207
|
const g = 갈래(f.path);
|
|
204
208
|
if (!g) continue;
|
|
205
209
|
셈.set(g, (셈.get(g) ?? 0) + 1);
|
|
@@ -210,8 +214,10 @@ export function 프로젝트갈래(뿌리, env = process.env) {
|
|
|
210
214
|
if (고르기(g, env)) { 답 = { 갈래: g, 대표파일: 첫파일.get(g), 개수: 셈.get(g) }; break; }
|
|
211
215
|
}
|
|
212
216
|
} catch { 답 = null; }
|
|
213
|
-
_센것.set(뿌리, 답);
|
|
214
217
|
return 답;
|
|
218
|
+
})();
|
|
219
|
+
_센것.set(뿌리, 약속);
|
|
220
|
+
return 약속;
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
/** 시험이 쓴다 — 폴더를 새로 만들고 다시 셀 때. */
|
package/src/repl.js
CHANGED
|
@@ -411,7 +411,7 @@ export async function chatLoop(opts = {}) {
|
|
|
411
411
|
* 답이 바뀌지 않는다. 아무것도 안 깔아 준다 — 있으면 쓰고 없으면 없는 대로
|
|
412
412
|
* Grep·Outline 으로 간다 (lsp/servers.js 머리말).
|
|
413
413
|
*/
|
|
414
|
-
session.lsp = 언어서버있나(root);
|
|
414
|
+
session.lsp = await 언어서버있나(root);
|
|
415
415
|
// 지난 대화에서 정해 둔 것을 들고 시작한다.
|
|
416
416
|
// 기억은 Session 이 켤 때 직접 읽는다 (session.js 생성자) — deel run 도 같은 것을 들고 시작하게.
|
|
417
417
|
|
package/src/safety/undo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 되돌리기. 승인 프롬프트를 안 쓰는 대신 이게 안전망이다.
|
|
2
2
|
// 파일을 고치기 전에 항상 이전 내용을 떠 놓고, /undo 로 턴 단위로 되돌린다.
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync, statSync, chmodSync } from 'node:fs';
|
|
5
5
|
import { looksBinary } from '../tools/encoding.js';
|
|
6
6
|
import { 말 } from '../i18n/index.js';
|
|
7
7
|
|
|
@@ -12,9 +12,40 @@ const KEEP_TURNS = 50;
|
|
|
12
12
|
export class History {
|
|
13
13
|
constructor(root) {
|
|
14
14
|
this.dir = join(root, '.deel', 'history');
|
|
15
|
-
|
|
15
|
+
// 폴더부터 본인 것으로. 파일만 잠그고 폴더가 0755 면 이름은 다 보인다.
|
|
16
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
16
17
|
this.file = join(this.dir, 'edits.jsonl');
|
|
17
18
|
this.turn = 0;
|
|
19
|
+
/** 잠근 결과. 밖에서 볼 수 있어야 「잠갔다」 는 말이 뜻을 갖는다. */
|
|
20
|
+
this.잠금 = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* ── 이 파일은 남의 파일 **원문**을 담는다 ───────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* 되돌리려면 고치기 전 내용을 그대로 들고 있어야 한다. 그래서 여기에는
|
|
27
|
+
* `.env` · `id_rsa` · `.npmrc` · kubeconfig 가 **평문으로** 들어온다.
|
|
28
|
+
* 가리지도 않는다 — 가리면 되돌릴 때 사람의 진짜 열쇠가 표로 덮여 없어진다.
|
|
29
|
+
* 즉 이 자리에서 지키는 방법은 파일 권한 하나뿐인데, 그것이 없었다.
|
|
30
|
+
*
|
|
31
|
+
* umask 022 인 리눅스·맥에서 이 파일은 0644 로 만들어진다. 같은 PC 의 다른
|
|
32
|
+
* 계정, 데몬, 공유 홈(NFS)의 아무나가 deel 이 손댄 모든 파일의 원문을 읽는다
|
|
33
|
+
* — 주인이 일부러 0600 으로 잠가 둔 파일까지. `.deel` 아래 다른 기록
|
|
34
|
+
* (agent/store.js · safety/audit.js)은 전부 잠그는데 여기만 안 잠갔다.
|
|
35
|
+
*
|
|
36
|
+
* 1.14.0 에서 셸 쓰기(`sed -i` · `echo >` · `tee`)까지 스냅샷을 뜨게 하면서
|
|
37
|
+
* 들어오는 파일이 더 늘었다. 고친 것이 이 구멍을 넓힌 셈이라 같이 막는다.
|
|
38
|
+
*
|
|
39
|
+
* 윈도우(NTFS)에서는 chmod 가 아무 일도 안 한다. 거기서는 못 잠근 것이지
|
|
40
|
+
* 잠근 척하지 않는다 — 그래서 결과를 `잠금` 에 있는 그대로 남긴다.
|
|
41
|
+
* (safety/audit.js · agent/store.js 와 같은 모양이다)
|
|
42
|
+
*/
|
|
43
|
+
#잠갔나 = false;
|
|
44
|
+
#잠그기() {
|
|
45
|
+
if (this.#잠갔나) return;
|
|
46
|
+
this.#잠갔나 = true;
|
|
47
|
+
try { chmodSync(this.file, 0o600); this.잠금 = { 모드: 0o600 }; }
|
|
48
|
+
catch (err) { this.잠금 = { 못함: err?.code ?? String(err) }; }
|
|
18
49
|
}
|
|
19
50
|
|
|
20
51
|
/**
|
|
@@ -73,6 +104,7 @@ export class History {
|
|
|
73
104
|
if (뜬것.enc) rec.enc = 뜬것.enc;
|
|
74
105
|
if (뜬것.skipped) rec.skipped = 뜬것.skipped;
|
|
75
106
|
appendFileSync(this.file, JSON.stringify(rec) + '\n', 'utf8');
|
|
107
|
+
this.#잠그기();
|
|
76
108
|
this.#이번턴.set(absPath, rec);
|
|
77
109
|
this.#maybePrune();
|
|
78
110
|
return rec;
|
|
@@ -109,6 +141,8 @@ export class History {
|
|
|
109
141
|
if (!버린수) return 0;
|
|
110
142
|
try {
|
|
111
143
|
writeFileSync(this.file, 남길것.map((r) => JSON.stringify(r)).join('\n') + (남길것.length ? '\n' : ''), 'utf8');
|
|
144
|
+
// 통째로 다시 쓰면 새 파일일 수 있다 — 빗장을 다시 건다 (agent/store.js 도 같다).
|
|
145
|
+
this.#잠갔나 = false; this.#잠그기();
|
|
112
146
|
} catch { return 0; }
|
|
113
147
|
return 버린수;
|
|
114
148
|
}
|
|
@@ -199,6 +233,7 @@ export class History {
|
|
|
199
233
|
const 못한것 = new Set(restored.filter((x) => x.ok === false).map((x) => x.path));
|
|
200
234
|
const keep = recs.filter((r) => !turns.includes(r.turn) || 못한것.has(r.path));
|
|
201
235
|
writeFileSync(this.file, keep.map((r) => JSON.stringify(r)).join('\n') + (keep.length ? '\n' : ''), 'utf8');
|
|
236
|
+
this.#잠갔나 = false; this.#잠그기();
|
|
202
237
|
return {
|
|
203
238
|
restored,
|
|
204
239
|
// 화면·감사기록이 쓰는 수. **진짜로 되돌아간 것만** 센다.
|
package/src/tools/fsutil.js
CHANGED
|
@@ -240,16 +240,44 @@ export function 훑기상한(env = process.env) {
|
|
|
240
240
|
/**
|
|
241
241
|
* 폴더를 훑는다.
|
|
242
242
|
*
|
|
243
|
+
/*
|
|
244
|
+
* ── 몇 개마다 한 번 숨을 쉴까 ───────────────────────────────────────────
|
|
245
|
+
*
|
|
246
|
+
* 숨 한 번은 setImmediate 한 번이고, 그 사이에 이벤트 루프가 한 바퀴 돈다.
|
|
247
|
+
* 그 한 바퀴가 이 파일에서 제일 중요한 줄이다 — 그동안에만 키보드가 배달되고,
|
|
248
|
+
* 그동안에만 signal.aborted 가 참으로 바뀔 수 있다.
|
|
249
|
+
*
|
|
250
|
+
* 자주 쉬면 훑기 자체가 느려지고, 드물게 쉬면 그만큼 귀를 닫는다. 200개면
|
|
251
|
+
* 사내망 드라이브에서도 한 숨 사이가 10ms 언저리다.
|
|
252
|
+
*/
|
|
253
|
+
export const 숨쉴간격 = 200;
|
|
254
|
+
const 한숨 = () => new Promise((풀기) => setImmediate(풀기));
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 폴더를 훑는다. **비동기다** — 부르는 쪽은 반드시 await 한다.
|
|
258
|
+
*
|
|
243
259
|
* @param {AbortSignal|null} signal 멈추라면 훑다 말고 나온다.
|
|
244
260
|
* 사내망 드라이브에서는 파일 20,000개 훑기가 몇 초다. 그 사이에
|
|
245
261
|
* ESC 를 눌렀는데 끝까지 다 훑고 나서 멈추면, 사람 눈에는 멈추지
|
|
246
262
|
* 않는 것으로 보인다. 나온 것에 `끊김` 을 달아 부르는 쪽이 알게 한다 —
|
|
247
263
|
* 조용히 적게 돌려주면 「그런 파일이 없다」가 되어 버린다.
|
|
264
|
+
*
|
|
265
|
+
* ── 왜 비동기여야 하나 ───────────────────────────────────────────────
|
|
266
|
+
*
|
|
267
|
+
* 여기 `signal?.aborted` 를 보는 줄은 오래 있었다. 그런데 훑기가 동기면
|
|
268
|
+
* 그 줄은 **부르기 전에 이미 켜져 있던 깃발**만 볼 수 있다. 깃발을 켜는
|
|
269
|
+
* 것은 이벤트 루프에 걸린 콜백인데, 동기 반복문이 도는 동안 이벤트 루프는
|
|
270
|
+
* 한 칸도 안 돈다.
|
|
271
|
+
*
|
|
272
|
+
* 그래서 「누르고 나서 시킨」 것은 멈추고 「시켜 놓고 누른」 것은 안 멈췄다.
|
|
273
|
+
* 사람이 실제로 하는 것은 뒤쪽인데. 지키는 것처럼 생겼는데 아무것도 안
|
|
274
|
+
* 지키는 줄이었다 (검사: test/abort-tools.test.js 의 4-2).
|
|
248
275
|
*/
|
|
249
|
-
export function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS, ignore = true, signal = null } = {}) {
|
|
276
|
+
export async function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS, ignore = true, signal = null } = {}) {
|
|
250
277
|
const out = [];
|
|
251
278
|
const 건너뜀 = { 폴더: 0, 파일: 0 };
|
|
252
279
|
let 끊김 = false;
|
|
280
|
+
let 본것 = 0;
|
|
253
281
|
const stack = [{ dir: root, rel: '', 규칙: ignore ? 뿌리규칙읽기(root) : [] }];
|
|
254
282
|
while (stack.length && out.length < limit) {
|
|
255
283
|
if (signal?.aborted) { 끊김 = true; break; }
|
|
@@ -276,7 +304,14 @@ export function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS, ignor
|
|
|
276
304
|
out.push({ path: full, rel: erel, mtime: st.mtimeMs, size: st.size });
|
|
277
305
|
if (out.length >= limit) break;
|
|
278
306
|
}
|
|
307
|
+
// 본 것을 다 센다 — 걸러낸 것도 readdirSync·걸리나 값을 이미 치렀다.
|
|
308
|
+
본것 += 1;
|
|
309
|
+
if (본것 % 숨쉴간격 === 0) {
|
|
310
|
+
await 한숨();
|
|
311
|
+
if (signal?.aborted) { 끊김 = true; break; }
|
|
312
|
+
}
|
|
279
313
|
}
|
|
314
|
+
if (끊김) break;
|
|
280
315
|
}
|
|
281
316
|
Object.defineProperty(out, '건너뜀', { value: 건너뜀, enumerable: false });
|
|
282
317
|
// 상한까지 찼으면 "여기서 멈췄다" 고 표시한다. 딱 맞아떨어져 끝난 경우까지
|
package/src/tools/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 도구. 이름과 인자를 Claude Code 와 같게 맞춘다 —
|
|
2
2
|
// 그래야 그 관례로 쓰인 스킬·명령이 그대로 먹는다.
|
|
3
|
-
import { writeFileSync, appendFileSync, readFileSync, existsSync, mkdirSync, statSync, renameSync, cpSync, rmSync
|
|
3
|
+
import { writeFileSync, appendFileSync, readFileSync, existsSync, mkdirSync, statSync, renameSync, cpSync, rmSync,
|
|
4
|
+
openSync, readSync, closeSync } from 'node:fs';
|
|
4
5
|
import { dirname, extname, join, relative, sep } from 'node:path';
|
|
5
6
|
import { 무리로돌리기 } from './spawn.js';
|
|
6
7
|
import { globToRegex, walk, readText, readTextFull, 내부살림 } from './fsutil.js';
|
|
@@ -226,10 +227,37 @@ function 가린표되돌리나(보인이름, 새내용) {
|
|
|
226
227
|
return 몇 ? 표막는말(보인이름, 몇) : null;
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
/**
|
|
231
|
+
* 파일 앞머리만 읽는다.
|
|
232
|
+
*
|
|
233
|
+
* 「글이 아닌 파일인가」 를 가리는 데 30MB 를 다 읽을 이유가 없다. 실제로
|
|
234
|
+
* looksBinary() 자체가 **앞 8000바이트만** 본다(encoding.js) — 그 뒤를 읽는
|
|
235
|
+
* 것은 답에 아무 영향이 없는 순수한 낭비다.
|
|
236
|
+
*/
|
|
237
|
+
function 앞머리(abs, 만큼) {
|
|
238
|
+
let fd = null;
|
|
239
|
+
try {
|
|
240
|
+
fd = openSync(abs, 'r');
|
|
241
|
+
const buf = Buffer.allocUnsafe(만큼);
|
|
242
|
+
const 읽은 = readSync(fd, buf, 0, 만큼, 0);
|
|
243
|
+
return buf.subarray(0, 읽은);
|
|
244
|
+
} catch { return null; } finally {
|
|
245
|
+
if (fd != null) { try { closeSync(fd); } catch { /* 이미 닫혔다 */ } }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// looksBinary 가 보는 만큼. 두 수가 어긋나면 답이 달라지므로 같이 움직여야 한다.
|
|
250
|
+
const 냄새맡을바이트 = 8000;
|
|
251
|
+
|
|
252
|
+
/** 파일이 몇 바이트인가. 못 재면 -1 — 그러면 들고 있던 수와 절대 안 맞는다. */
|
|
253
|
+
function 파일크기(abs) {
|
|
254
|
+
try { return statSync(abs).size; } catch { return -1; }
|
|
255
|
+
}
|
|
256
|
+
|
|
229
257
|
function 바이너리인가(abs) {
|
|
230
258
|
if (!existsSync(abs)) return null;
|
|
231
|
-
|
|
232
|
-
|
|
259
|
+
const buf = 앞머리(abs, 냄새맡을바이트);
|
|
260
|
+
if (!buf) return null;
|
|
233
261
|
if (!looksBinary(buf)) return null;
|
|
234
262
|
return `글이 아닌 파일입니다 — 덮어쓰면 되살릴 수 없습니다.\n`
|
|
235
263
|
+ ' 그림·hwp·pdf·압축파일 같은 것을 글로 덮어쓰면 그 파일은 그대로 끝납니다.\n'
|
|
@@ -352,13 +380,44 @@ function 바꾸기전스냅샷(cmd, ctx) {
|
|
|
352
380
|
}
|
|
353
381
|
|
|
354
382
|
/** 지금 파일이 몇 줄인가. 붙인 뒤 '얼마나 찼는지' 를 사실로 말해 주려고 센다. */
|
|
355
|
-
function 줄수(abs, 인코딩) {
|
|
383
|
+
function 줄수(abs, 인코딩) { return 줄재기(abs, 인코딩).줄; }
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 줄 수와 「끝이 줄바꿈인가」 를 한 번에.
|
|
387
|
+
*
|
|
388
|
+
* 둘을 따로 재면 파일을 두 번 읽는다. 그리고 이어 붙일 때는 둘 다 필요하다 —
|
|
389
|
+
* 끝이 줄바꿈이 아니면 붙이는 첫 줄이 **마지막 줄에 이어지므로** 한 줄 덜 는다.
|
|
390
|
+
*/
|
|
391
|
+
function 줄재기(abs, 인코딩) {
|
|
356
392
|
try {
|
|
357
393
|
const buf = readFileSync(abs);
|
|
358
|
-
if (looksBinary(buf)) return 0;
|
|
394
|
+
if (looksBinary(buf)) return { 줄: 0, 끝줄바꿈: true };
|
|
359
395
|
const t = 인코딩 && 인코딩 !== 'utf-8' ? decodeBytes(buf).text : buf.toString('utf8');
|
|
360
|
-
|
|
361
|
-
|
|
396
|
+
const 끝줄바꿈 = t.length === 0 || t.endsWith('\n');
|
|
397
|
+
return { 줄: t.split('\n').length - (t.endsWith('\n') ? 1 : 0), 끝줄바꿈 };
|
|
398
|
+
} catch { return { 줄: 0, 끝줄바꿈: true }; }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/*
|
|
402
|
+
* ── 이어 붙일 때 줄 수를 다시 안 세려고 들고 있는 것 ─────────────────────
|
|
403
|
+
*
|
|
404
|
+
* Append 는 **한 파일에 여러 번** 불리는 것이 정상이다 — 도구 설명문이 그렇게
|
|
405
|
+
* 시킨다. 그런데 부를 때마다 「지금 전체 몇 줄」 을 대려고 파일을 통째로 읽고
|
|
406
|
+
* 해독하고 split 했다. 30MB 로그에 다섯 줄 붙이는 데 그 짓을 다섯 번 한다.
|
|
407
|
+
*
|
|
408
|
+
* 붙인 줄 수는 우리가 안다. 그러니 앞의 수에 더하면 된다.
|
|
409
|
+
*
|
|
410
|
+
* 그 수가 아직 참인지는 **바이트 수**로 가린다. 우리가 붙인 만큼만 커졌으면
|
|
411
|
+
* 우리가 아는 그 파일이다. 사람이 편집기로 갈아엎었거나 다른 도구가 건드렸으면
|
|
412
|
+
* 크기가 안 맞고, 그때는 두말없이 다시 센다. 틀린 수를 빠르게 대는 것보다
|
|
413
|
+
* 맞는 수를 느리게 대는 편이 낫다.
|
|
414
|
+
*/
|
|
415
|
+
const 줄기억 = new Map();
|
|
416
|
+
// 파일이 수천 개씩 쌓이지는 않지만, 오래 도는 대화에서 무한정 크지도 않게 한다.
|
|
417
|
+
const 줄기억최대 = 512;
|
|
418
|
+
function 줄기억넣기(abs, 것) {
|
|
419
|
+
if (줄기억.size >= 줄기억최대) 줄기억.delete(줄기억.keys().next().value);
|
|
420
|
+
줄기억.set(abs, 것);
|
|
362
421
|
}
|
|
363
422
|
|
|
364
423
|
/**
|
|
@@ -585,7 +644,7 @@ export function 복사해옮기기(앞, 뒤, { 복사 = cpSync, 지우기 = rmSy
|
|
|
585
644
|
return {};
|
|
586
645
|
}
|
|
587
646
|
|
|
588
|
-
function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
647
|
+
async function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
589
648
|
if (typeof from !== 'string' || !from) return { error: 'from 이 없습니다' };
|
|
590
649
|
if (typeof to !== 'string' || !to) return { error: 'to 가 없습니다' };
|
|
591
650
|
|
|
@@ -626,7 +685,7 @@ function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
626
685
|
* 뜨는 쪽이 훨씬 나쁘다.
|
|
627
686
|
*/
|
|
628
687
|
// 옮길 때는 .gitignore 를 안 본다 — 옮겨지는 것은 전부이고, 되돌리기도 전부를 떠야 한다.
|
|
629
|
-
const 훑은것 = 폴더인가 ? walk(앞, { ignore: false }) : null;
|
|
688
|
+
const 훑은것 = 폴더인가 ? await walk(앞, { ignore: false }) : null;
|
|
630
689
|
const 짝들 = 폴더인가
|
|
631
690
|
? 훑은것.map((f) => [f.path, join(뒤, relative(앞, f.path))])
|
|
632
691
|
: [[앞, 뒤]];
|
|
@@ -688,7 +747,7 @@ function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
688
747
|
}
|
|
689
748
|
|
|
690
749
|
/** 여러 개를 한 번에. 하나가 막혀도 나머지는 옮기고, 무엇이 막혔는지 다 알린다. */
|
|
691
|
-
function 여러개옮기기(목록, ctx) {
|
|
750
|
+
async function 여러개옮기기(목록, ctx) {
|
|
692
751
|
const 된것 = [];
|
|
693
752
|
const 안된것 = [];
|
|
694
753
|
/*
|
|
@@ -704,7 +763,7 @@ function 여러개옮기기(목록, ctx) {
|
|
|
704
763
|
*/
|
|
705
764
|
const 바뀐것들 = [];
|
|
706
765
|
for (const 하나 of 목록) {
|
|
707
|
-
const r = 한개옮기기(하나, ctx);
|
|
766
|
+
const r = await 한개옮기기(하나, ctx);
|
|
708
767
|
if (r.error) { 안된것.push(`${하나.from ?? '?'} → ${하나.to ?? '?'}: ${r.error}`); continue; }
|
|
709
768
|
된것.push(r.content);
|
|
710
769
|
// 폴더를 옮겼으면 그 안의 파일 하나하나가, 아니면 옮겨 간 자리가 답이다.
|
|
@@ -755,13 +814,38 @@ function 한파일쓰기(args, ctx) {
|
|
|
755
814
|
const existed = existsSync(abs);
|
|
756
815
|
// 덮어쓰기 전 내용. 바뀐 자리를 보여주려면 지금 떠 놔야 한다.
|
|
757
816
|
// 읽다 터지는 파일(바이너리 등)이면 그냥 없던 셈 친다 — 쓰는 것 자체는 막지 않는다.
|
|
758
|
-
|
|
759
|
-
|
|
817
|
+
/*
|
|
818
|
+
* 읽은 것을 **통째로** 들고 있는다 — 글만 뽑고 버리지 않는다.
|
|
819
|
+
*
|
|
820
|
+
* 여기가 `.text` 만 꺼내 쓰고 `.encoding` 을 버리고 있었다. 그래서 바로
|
|
821
|
+
* 아래에서 인코딩을 다시 정할 때 볼 것이 캐시밖에 없었고, 캐시는 이
|
|
822
|
+
* 파일을 **Read 로 한 번이라도 연 적이 있을 때만** 차 있다.
|
|
823
|
+
*
|
|
824
|
+
* 「이 파일 전체를 다시 써 줘」 → 모델은 Read 없이 Write 로 간다
|
|
825
|
+
* → 캐시가 비어 있다 → utf-8 로 때운다
|
|
826
|
+
* → 사내 CP949 문서가 조용히 UTF-8 이 된다
|
|
827
|
+
*
|
|
828
|
+
* 화면에는 `덮어씀: 사내문서.txt (4줄)` 만 뜬다. 인코딩 표기는 「원래가
|
|
829
|
+
* utf-8 이 아닐 때」 붙는데 그 원래가 이미 틀렸으니 한 글자도 안 뜬다.
|
|
830
|
+
* 사내 뷰어에서 한글이 깨지고 .bat 이 안 도는데, 사람은 「한 줄 고쳐
|
|
831
|
+
* 달라」 고 했을 뿐이라 원인을 인코딩으로 이을 길이 없다.
|
|
832
|
+
*/
|
|
833
|
+
let 읽음 = null;
|
|
834
|
+
if (existed) { try { 읽음 = readTextFull(abs); } catch { 읽음 = null; } }
|
|
835
|
+
// 덮어쓰기 전 내용. 바뀐 자리를 보여주려면 지금 떠 놔야 한다.
|
|
836
|
+
const 이전 = 읽음?.text ?? null;
|
|
760
837
|
mkdirSync(dirname(abs), { recursive: true });
|
|
761
838
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
839
|
+
/*
|
|
840
|
+
* 원래 있던 파일이면 그 파일이 **지금** 쓰는 인코딩으로 되돌려 쓴다.
|
|
841
|
+
* 새 파일이면 UTF-8 이다 — 요즘 만드는 파일까지 옛 인코딩으로 둘 이유가 없다.
|
|
842
|
+
*
|
|
843
|
+
* 캐시(ctx.enc)는 여기서 안 본다. 방금 잰 값이 있는데 캐시를 보면 두 가지가
|
|
844
|
+
* 어긋난다 — 캐시가 비면 UTF-8 로 때우고(위), 캐시가 낡으면 남이 이미
|
|
845
|
+
* UTF-8 로 바꿔 둔 파일을 CP949 라고 우기며 이모지를 거절한다. 재는 값이
|
|
846
|
+
* 있는 자리에서는 잰 값이 이긴다. Edit 도 이미 그렇게 한다.
|
|
847
|
+
*/
|
|
848
|
+
const 원래 = 읽음?.encoding ?? 'utf-8';
|
|
765
849
|
const 만든것 = encode(args.content, 원래);
|
|
766
850
|
if (만든것.lost.length) {
|
|
767
851
|
return {
|
|
@@ -1242,6 +1326,9 @@ export const TOOLS = {
|
|
|
1242
1326
|
// 이어 붙이는 조각에는 앞머리 표식(BOM)이 들어가면 안 된다 — 파일 한가운데에
|
|
1243
1327
|
// BOM 이 박히면 그 자리가 이상한 글자로 보인다. 그래서 표식 없는 이름으로 바꾼다.
|
|
1244
1328
|
const 원래 = existed ? (ctx.enc?.get(abs) ?? 재는인코딩(abs)) : 'utf-8';
|
|
1329
|
+
// 잰 것은 적어 둔다 — Read 가 하는 것과 같은 자리다. 안 적어 두면 이 파일에
|
|
1330
|
+
// 붙일 때마다 인코딩을 다시 재느라 파일을 통째로 또 읽는다.
|
|
1331
|
+
ctx.enc?.set?.(abs, 원래);
|
|
1245
1332
|
const 조각인코딩 = 원래 === 'utf-8-bom' ? 'utf-8' : 원래;
|
|
1246
1333
|
const 만든것 = encode(args.content, 조각인코딩);
|
|
1247
1334
|
if (만든것.lost.length) {
|
|
@@ -1252,13 +1339,30 @@ export const TOOLS = {
|
|
|
1252
1339
|
};
|
|
1253
1340
|
}
|
|
1254
1341
|
|
|
1342
|
+
/*
|
|
1343
|
+
* 붙이기 **전에** 앞의 줄 수를 잡아 둔다.
|
|
1344
|
+
*
|
|
1345
|
+
* 들고 있던 것이 있고 크기가 그대로면 그걸 쓴다. 아니면 여기서 한 번
|
|
1346
|
+
* 세고, 다음 번부터는 더하기만 한다. 없던 파일이면 앞이 0줄이다.
|
|
1347
|
+
*/
|
|
1348
|
+
const 앞것 = !existed
|
|
1349
|
+
? { 줄: 0, 끝줄바꿈: true }
|
|
1350
|
+
: (() => {
|
|
1351
|
+
const 크기 = 파일크기(abs);
|
|
1352
|
+
const 든것 = 줄기억.get(abs);
|
|
1353
|
+
if (든것 && 든것.바이트 === 크기) return 든것;
|
|
1354
|
+
const 잰것 = 줄재기(abs, 원래);
|
|
1355
|
+
return { 바이트: 크기, ...잰것 };
|
|
1356
|
+
})();
|
|
1357
|
+
|
|
1255
1358
|
mkdirSync(dirname(abs), { recursive: true });
|
|
1256
1359
|
if (existed) appendFileSync(abs, 만든것.buf);
|
|
1257
1360
|
else writeFileSync(abs, 만든것.buf);
|
|
1258
1361
|
ctx.seen.add(abs);
|
|
1259
1362
|
|
|
1260
1363
|
const 붙인줄 = args.content.split('\n').length - (args.content.endsWith('\n') ? 1 : 0);
|
|
1261
|
-
const 전체줄 = 줄수(abs, 원래);
|
|
1364
|
+
const 전체줄 = 앞것 ? 앞것.줄 + 붙인줄 - (앞것.끝줄바꿈 ? 0 : 1) : 줄수(abs, 원래);
|
|
1365
|
+
줄기억넣기(abs, { 바이트: 파일크기(abs), 줄: 전체줄, 끝줄바꿈: args.content.endsWith('\n') });
|
|
1262
1366
|
const 표기 = 원래 !== 'utf-8' ? ` · ${encLabel(원래)}` : '';
|
|
1263
1367
|
return {
|
|
1264
1368
|
content: `${existed ? '이어 붙임' : '새로 만듦'}: ${ctx.scope.show(abs)}`
|
|
@@ -1366,15 +1470,15 @@ export const TOOLS = {
|
|
|
1366
1470
|
required: [],
|
|
1367
1471
|
},
|
|
1368
1472
|
},
|
|
1369
|
-
run(args, ctx) {
|
|
1473
|
+
async run(args, ctx) {
|
|
1370
1474
|
const 목록 = Array.isArray(args.moves) ? args.moves.filter((x) => x && typeof x === 'object') : [];
|
|
1371
1475
|
if (목록.length) {
|
|
1372
|
-
return 여러개옮기기(목록.map((m) => ({ ...m, overwrite: m.overwrite ?? args.overwrite })), ctx);
|
|
1476
|
+
return await 여러개옮기기(목록.map((m) => ({ ...m, overwrite: m.overwrite ?? args.overwrite })), ctx);
|
|
1373
1477
|
}
|
|
1374
1478
|
if (typeof args.from !== 'string' || !args.from) {
|
|
1375
1479
|
return { error: 'from 이 없습니다. 한 개면 from·to 를, 여러 개면 moves 배열을 주세요.' };
|
|
1376
1480
|
}
|
|
1377
|
-
return 한개옮기기(args, ctx);
|
|
1481
|
+
return await 한개옮기기(args, ctx);
|
|
1378
1482
|
},
|
|
1379
1483
|
},
|
|
1380
1484
|
|
|
@@ -1391,10 +1495,10 @@ export const TOOLS = {
|
|
|
1391
1495
|
required: ['pattern'],
|
|
1392
1496
|
},
|
|
1393
1497
|
},
|
|
1394
|
-
run(args, ctx) {
|
|
1498
|
+
async run(args, ctx) {
|
|
1395
1499
|
const root = args.path ? ctx.scope.resolve(args.path) : ctx.scope.root;
|
|
1396
1500
|
const re = globToRegex(args.pattern);
|
|
1397
|
-
const 전부 = walk(root, { signal: ctx.signal });
|
|
1501
|
+
const 전부 = await walk(root, { signal: ctx.signal });
|
|
1398
1502
|
// 훑다 말고 나왔으면 그렇다고 말한다. 조용히 적게 주면 「그런 파일이 없다」가 된다.
|
|
1399
1503
|
if (전부.끊김) return { error: '중단했습니다. 폴더를 끝까지 안 훑었습니다.', 끝났다: true, 중단됨: true };
|
|
1400
1504
|
// .gitignore 로 건너뛴 것은 수를 말한다 — 조용히 빼면 '그 파일이 없다' 로 읽힌다 (tools/ignore.js).
|
|
@@ -1526,7 +1630,7 @@ export const TOOLS = {
|
|
|
1526
1630
|
*/
|
|
1527
1631
|
let files = isFile
|
|
1528
1632
|
? [{ path: root, rel: ctx.scope.show(root) }]
|
|
1529
|
-
: walk(root, { signal: ctx.signal });
|
|
1633
|
+
: await walk(root, { signal: ctx.signal });
|
|
1530
1634
|
if (files.끊김) return { error: '중단했습니다. 폴더를 끝까지 안 훑었습니다.', 끝났다: true, 중단됨: true };
|
|
1531
1635
|
const 안본것 = isFile ? '' : 건너뜀말(files.건너뜀, files.잘림, files.상한).trim(); // .gitignore 로 건너뛴 수 — 꼬리에 적는다
|
|
1532
1636
|
// 훑기 상한에 걸렸으면 "일치 없음" 이라고 잘라 말하면 안 된다. 안 본 것이다.
|
|
@@ -2478,8 +2582,8 @@ async function 고친뒤진단(name, r, ctx) {
|
|
|
2478
2582
|
}
|
|
2479
2583
|
|
|
2480
2584
|
/** 이 폴더에서 언어 서버를 쓸 수 있나. repl 이 켤 때 한 번 물어본다. */
|
|
2481
|
-
export function 언어서버있나(뿌리) {
|
|
2482
|
-
try { return
|
|
2585
|
+
export async function 언어서버있나(뿌리) {
|
|
2586
|
+
try { return !!(await 프로젝트갈래(뿌리)); } catch { return false; }
|
|
2483
2587
|
}
|
|
2484
2588
|
|
|
2485
2589
|
export async function runTool(name, args, ctx) {
|
package/src/tools/lsp.js
CHANGED
|
@@ -179,7 +179,7 @@ async function 채비(args, ctx) {
|
|
|
179
179
|
* 간다 — 모델은 `handleClick 어디 있어` 처럼 이름만 알고 부르는 것이 보통이라,
|
|
180
180
|
* 매번 파일을 요구하면 이 도구를 쓰는 값이 사라진다.
|
|
181
181
|
*/
|
|
182
|
-
const 볼것 = 파일 ?? 프로젝트갈래(ctx.scope.root)?.대표파일 ?? null;
|
|
182
|
+
const 볼것 = 파일 ?? (await 프로젝트갈래(ctx.scope.root))?.대표파일 ?? null;
|
|
183
183
|
if (!볼것) return { 오류: '이 폴더에서 쓸 수 있는 언어 서버가 없습니다. Grep · Outline 을 쓰세요.' };
|
|
184
184
|
if (!갈래(볼것)) return { 오류: `언어 서버가 없는 갈래입니다: ${볼것}` };
|
|
185
185
|
|
package/src/tools/outline.js
CHANGED
|
@@ -229,14 +229,14 @@ export const OUTLINE_TOOL = {
|
|
|
229
229
|
},
|
|
230
230
|
},
|
|
231
231
|
|
|
232
|
-
run(args, ctx) {
|
|
232
|
+
async run(args, ctx) {
|
|
233
233
|
const 시작 = args.path ? ctx.scope.resolve(args.path) : ctx.scope.root;
|
|
234
234
|
if (!existsSync(시작)) return { error: `없는 경로입니다: ${args.path ?? '.'}` };
|
|
235
235
|
|
|
236
236
|
const 하나인가 = statSync(시작).isFile();
|
|
237
237
|
let 파일들 = 하나인가
|
|
238
238
|
? [{ path: 시작, rel: ctx.scope.show(시작), mtime: statSync(시작).mtimeMs }]
|
|
239
|
-
: walk(시작, { skipDirs: SKIP_DIRS, signal: ctx.signal });
|
|
239
|
+
: await walk(시작, { skipDirs: SKIP_DIRS, signal: ctx.signal });
|
|
240
240
|
if (파일들.끊김) return { error: '중단했습니다. 폴더를 끝까지 안 훑었습니다.', 끝났다: true, 중단됨: true };
|
|
241
241
|
const 건너뜀 = 하나인가 ? '' : 건너뜀말(파일들.건너뜀, 파일들.잘림, 파일들.상한); // .gitignore 로 건너뛴 수 (tools/ignore.js)
|
|
242
242
|
|
package/src/tools/verify.js
CHANGED
|
@@ -194,13 +194,13 @@ export const VERIFY_TOOL = {
|
|
|
194
194
|
try { abs = ctx.scope.resolve(p); } catch (e) { 볼것.push({ 없음: p, 왜: e.message }); continue; }
|
|
195
195
|
if (!existsSync(abs)) { 볼것.push({ 없음: p, 왜: '파일이 없습니다' }); continue; }
|
|
196
196
|
if (statSync(abs).isDirectory()) {
|
|
197
|
-
const 안것 = walk(abs, { skipDirs: SKIP_DIRS });
|
|
197
|
+
const 안것 = await walk(abs, { skipDirs: SKIP_DIRS });
|
|
198
198
|
셈더하기(안것);
|
|
199
199
|
for (const f of 안것) 볼것.push({ path: f.path });
|
|
200
200
|
} else 볼것.push({ path: abs }); // 짚어 준 파일은 규칙과 상관없이 본다
|
|
201
201
|
}
|
|
202
202
|
} else {
|
|
203
|
-
const 전부 = walk(뿌리, { skipDirs: SKIP_DIRS });
|
|
203
|
+
const 전부 = await walk(뿌리, { skipDirs: SKIP_DIRS });
|
|
204
204
|
셈더하기(전부);
|
|
205
205
|
const 볼만한것 = 전부
|
|
206
206
|
.filter((f) => ['.html', '.htm', '.css', '.json', '.js', '.mjs', '.cjs', '.py'].includes(extname(f.path).toLowerCase()));
|