deel-local-cli 1.8.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.ko.md +10 -25
  2. package/README.md +43 -51
  3. package/bin/deel.js +56 -13
  4. package/package.json +60 -60
  5. package/src/acp/serve.js +810 -732
  6. package/src/agent/budget.js +156 -153
  7. package/src/agent/compact.js +420 -296
  8. package/src/agent/effort.js +308 -177
  9. package/src/agent/evidence.js +2 -0
  10. package/src/agent/evolve.js +263 -213
  11. package/src/agent/filemem.js +155 -0
  12. package/src/agent/grade.js +51 -1
  13. package/src/agent/loop.js +1707 -1349
  14. package/src/agent/memory.js +156 -152
  15. package/src/agent/mention.js +210 -210
  16. package/src/agent/session.js +1049 -832
  17. package/src/agent/store.js +186 -9
  18. package/src/agent/threads.js +26 -1
  19. package/src/backend/adapter.js +1159 -615
  20. package/src/backend/cachemark.js +150 -0
  21. package/src/backend/detect.js +327 -288
  22. package/src/backend/learn.js +24 -0
  23. package/src/backend/mcp.js +96 -5
  24. package/src/backend/probe.js +131 -61
  25. package/src/backend/quota.js +250 -133
  26. package/src/backend/retry.js +30 -6
  27. package/src/backend/tokens.js +37 -0
  28. package/src/backend/toolfit.js +352 -0
  29. package/src/backend/wire.js +600 -0
  30. package/src/commands.js +3135 -2918
  31. package/src/config.js +47 -2
  32. package/src/i18n/en.js +544 -466
  33. package/src/i18n/index.js +18 -0
  34. package/src/i18n/ja.js +497 -417
  35. package/src/i18n/ko.js +591 -466
  36. package/src/i18n/zh.js +497 -417
  37. package/src/lsp/client.js +49 -5
  38. package/src/oneshot.js +620 -471
  39. package/src/pack/sbom.js +30 -4
  40. package/src/pack/selfpack.js +25 -9
  41. package/src/pack/sheet.en.js +288 -0
  42. package/src/pack/tar.js +65 -2
  43. package/src/plugins/manage.js +46 -9
  44. package/src/providers/bedrock.js +17 -0
  45. package/src/repl.js +2602 -2286
  46. package/src/safety/audit.js +92 -8
  47. package/src/safety/authcmd.js +14 -3
  48. package/src/safety/guard.js +143 -0
  49. package/src/safety/keystore.js +62 -39
  50. package/src/safety/undo.js +24 -6
  51. package/src/tools/fsutil.js +265 -250
  52. package/src/tools/index.js +239 -34
  53. package/src/tools/jobs.js +158 -29
  54. package/src/tools/verify.js +358 -328
  55. package/src/tools/webfetch.js +92 -9
  56. package/src/ui/md.js +201 -5
  57. package/src/ui/motion.js +0 -1
  58. package/src/ui/pastechip.js +50 -3
  59. package/src/ui/pick.js +115 -0
  60. package/src/ui/screen.js +23 -3
  61. package/src/ui/status.js +613 -610
@@ -1,250 +1,265 @@
1
- // 파일 훑기와 glob 매칭. 외부 패키지 없이 직접 구현한다.
2
- import { readdirSync, statSync, readFileSync, mkdirSync, copyFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { decode, looksBinary } from './encoding.js';
5
- import { 뿌리규칙읽기, 파일규칙읽기, 걸리나 } from './ignore.js';
6
-
7
- /**
8
- * 폴더를 통째로 옮겨 담는다.
9
- *
10
- * fs.cpSync 를 안 쓰는 이유가 둘이다.
11
- *
12
- * 1) Node 가 실험 기능으로 표시한 API 다. 판마다 동작이 다르고, 윈도우에서
13
- * 프로세스가 통째로 죽는 것을 실제로 겪었다 — 검사가 아무 말도 없이
14
- * 0xC0000409 로 끝났다. 배포되는 코드가 실험 API 에 매달려 있으면 안 된다.
15
- *
16
- * 2) cpSync 는 심볼릭 링크를 따라간다. 남이 준 플러그인 폴더에 바깥을 가리키는
17
- * 링크가 하나 있으면 그것까지 딸려 들어온다. 여기서는 링크를 건너뛴다 —
18
- * 플러그인은 제 폴더 안의 글 파일이면 충분하다.
19
- *
20
- * 하는 일이 뻔해서 읽으면 다 보인다. 그게 이 프로젝트가 원하는 것이다.
21
- */
22
- export function copyDir(from, to, { skipped = [] } = {}) {
23
- mkdirSync(to, { recursive: true });
24
- for (const e of readdirSync(from, { withFileTypes: true })) {
25
- const s = join(from, e.name);
26
- const d = join(to, e.name);
27
- if (e.isSymbolicLink()) { skipped.push(s); continue; }
28
- if (e.isDirectory()) copyDir(s, d, { skipped });
29
- else if (e.isFile()) copyFileSync(s, d);
30
- // 그 밖(장치·소켓 같은 것)은 건너뛴다. 플러그인에 있을 이유가 없다.
31
- else skipped.push(s);
32
- }
33
- return { skipped };
34
- }
35
-
36
- /**
37
- * 다른 코딩 도구들이 제 살림을 넣어 두는 자리.
38
- *
39
- * 프로젝트 파일이 아니라 그 도구의 기록이다 — 지난 대화, 명령 이력, 캐시,
40
- * 그리고 열쇠. 이 작업과 아무 상관이 없는데 훑을 때 걸려 나오면 모델이
41
- * 그것부터 읽는다. 실제로 .claude/history.jsonl 을 읽어 컨텍스트를 채운 적이 있다.
42
- *
43
- * **목록은 여기 한 곳에만 둔다.** 전에는 훑는 쪽과 읽기 막는 쪽에 따로 적어
44
- * 놨는데, 그러면 한쪽에만 새 이름을 넣는 날이 반드시 온다. 훑을 때는 안 걸리는데
45
- * 이름을 대면 읽히는, 설명하기 어려운 상태가 된다.
46
- *
47
- * 새 도구는 계속 나온다. 여기 없는 이름이 보이면 그냥 한 줄 더하면 된다.
48
- */
49
- export const 남의도구살림 = new Set([
50
- '.claude', '.codex', '.cursor', '.gemini', '.aider', '.continue', '.cline',
51
- '.roo', '.kilocode', '.windsurf', '.opencode', '.zed', '.trae', '.augment',
52
- '.qodo', '.tabnine', '.cody', '.sourcegraph', '.copilot', '.amazonq', '.junie',
53
- '.codeium', '.goose', '.crush', '.gptme', '.openhands', '.devin',
54
- ]);
55
-
56
- // .aider.chat.history.md 처럼 폴더가 아니라 파일로 흘리는 것들도 있다.
57
- export const 남의도구파일 = /^\.(aider|copilot|continue|cline|codeium|windsurf)[.-]/i;
58
-
59
- export const SKIP_DIRS = new Set([
60
- 'node_modules', '.git', '.deel', '.svn', '.hg', 'dist', 'build',
61
- '.next', '.nuxt', '.cache', '__pycache__', '.venv', 'venv', 'target',
62
- ...남의도구살림,
63
- ]);
64
-
65
- /**
66
- * 이 파일이 '읽어 봐야 도움이 안 되는 살림' 인가.
67
- *
68
- * 왜 막나:
69
- * 실제로 이런 일이 있었다 —
70
- * ◧ Read(~/.deel/audit.jsonl) 77줄
71
- * ◧ Read(~/.claude/history.jsonl) 35줄
72
- * 감사기록은 이 프로그램이 방금 무엇을 했는지 적어 둔 것이다. 그걸 다시 읽어
73
- * 대화에 넣으면 모델이 제 그림자를 좇는다. 사용자가 시킨 일과는 아무 상관이
74
- * 없고, 컨텍스트만 찬다.
75
- *
76
- * 설정 파일은 더 나쁘다. 게이트웨이 열쇠(apiKey)가 그 안에 있다.
77
- * 읽는 순간 그 열쇠가 대화에 실려 모델로 나가고, 세션 기록으로 디스크에도 남는다.
78
- * 열쇠를 그 열쇠의 주인에게 보내는 셈이다.
79
- *
80
- * 막는 것이지 숨기는 것이 아니다 — 왜 안 되는지 그대로 말해 준다.
81
- * @returns {string|null} 막을 이유. 막을 것이 아니면 null.
82
- */
83
- export function 내부살림(abs) {
84
- const 조각 = String(abs ?? '').replace(/\\/g, '/').split('/');
85
- const 이름 = 조각[조각.length - 1] ?? '';
86
- const i = 조각.lastIndexOf('.deel');
87
- if (i >= 0) {
88
- const 안 = 조각.slice(i + 1);
89
- if (안[0] === 'config.json') {
90
- return 'deel 자신의 설정 파일입니다. 게이트웨이 열쇠가 들어 있어 읽지 않습니다.'
91
- + ' 연결 상태가 궁금하면 사용자에게 /status 를 쳐 보라고 하세요.';
92
- }
93
- if (['audit.jsonl', 'sessions', 'history'].includes(안[0])) {
94
- return 'deel 자신의 기록입니다(무엇을 했는지 적어 둔 것). 지금 하는 일에 도움이 안 되고'
95
- + ' 컨텍스트만 차서 읽지 않습니다. 필요한 것은 대화에 이미 다 있습니다.';
96
- }
97
- }
98
- // 남의 도구 살림. 목록은 남의도구살림 곳에만 있다 훑는 쪽과 같은 것을 본다.
99
- const 걸린것 = 조각.find((seg) => 남의도구살림.has(seg)) ?? (남의도구파일.test(이름) ? 이름 : null);
100
- if (걸린것) {
101
- return `${걸린것} 다른 코딩 도구가 기록을 넣어 두는 자리입니다.`
102
- + ' 지난 대화·명령 이력·열쇠 같은 것이라 이 작업과 상관이 없고, 읽으면 컨텍스트만 찹니다.'
103
- + ' 정말 그 안의 내용이 필요하면 사용자에게 직접 물어보세요.';
104
- }
105
- if (이름 === 'audit.jsonl') {
106
- return 'deel 자신의 기록입니다. 읽어도 지금 하는 일에 도움이 됩니다.';
107
- }
108
- return null;
109
- }
110
-
111
- // glob 을 정규식으로. ** * ? {a,b} [abc] 를 지원한다.
112
- export function globToRegex(pattern) {
113
- let re = '';
114
- let i = 0;
115
- const p = pattern.replace(/\\/g, '/');
116
- while (i < p.length) {
117
- const ch = p[i];
118
- if (ch === '*') {
119
- if (p[i + 1] === '*') {
120
- // **/ "0개 이상의 폴더", ** 는 "무엇이든"
121
- if (p[i + 2] === '/') { re += '(?:[^/]*\\/)*'; i += 3; }
122
- else { re += '.*'; i += 2; }
123
- } else { re += '[^/]*'; i += 1; }
124
- } else if (ch === '?') { re += '[^/]'; i += 1; }
125
- else if (ch === '{') {
126
- const end = p.indexOf('}', i);
127
- if (end < 0) { re += '\\{'; i += 1; }
128
- else {
129
- const parts = p.slice(i + 1, end).split(',').map(escapeLiteral);
130
- re += `(?:${parts.join('|')})`;
131
- i = end + 1;
132
- }
133
- } else if (ch === '[') {
134
- const end = p.indexOf(']', i);
135
- if (end < 0) { re += '\\['; i += 1; }
136
- else { re += p.slice(i, end + 1); i = end + 1; }
137
- } else { re += escapeLiteral(ch); i += 1; }
138
- }
139
- return new RegExp(`^${re}$`, process.platform === 'win32' ? 'i' : '');
140
- }
141
-
142
- function escapeLiteral(s) {
143
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
144
- }
145
-
146
- /**
147
- * 폴더를 훑어 파일 목록을 낸다. { path(절대), rel(/구분), mtime, size }
148
- *
149
- * `ignore` 가 켜져 있으면(기본) .gitignore 가 건너뛰라는 것은 건너뛴다 (tools/ignore.js).
150
- * skipDirs 아래의 바닥이다 .gitignore 없어도 node_modules 는 늘 건너뛴다.
151
- * 돌려주는 배열에는 `건너뜀 = { 폴더, 파일 }` (열거되지 않게) 붙어 있다. 부르는 쪽이 수를
152
- * 화면에 적는다 조용히 빼면 "그 파일이 없다" 로 읽힌다.
153
- * 폴더를 통째로 옮기거나 복사할 때는 `ignore: false` 로 — 그때는 다 있어야 한다.
154
- *
155
- * `잘림` 도 같이 붙는다 — **상한에서 멈췄다는 뜻이다.**
156
- *
157
- * 전에는 이 상한이 조용했다. 파일 5만 개짜리 저장소에서 앞의 2만 개만 훑고
158
- * 멈추는데, 아무 데도 그 말이 안 나갔다. 그래서 `Grep` 이 "일치 없음" 이라고
159
- * 답했다 — 실제로는 뒤쪽 3만 개에 100군데가 있는데도. 못 찾은 것과 안 본 것은
160
- * 다르고, 그걸 안 가르면 사람은 없는 줄 알고 그냥 간다. 조용한 상한은
161
- * 틀린 답보다 나쁘다.
162
- */
163
- export const 기본훑기상한 = 20000;
164
-
165
- /**
166
- * 개까지 훑을까.
167
- *
168
- * 2만 개는 대부분의 저장소에 넉넉하지만 단일 저장소에는 모자란다. 그래서
169
- * 올릴 수 있게 열어 둔다 — 못 보는 것을 알려 주기만 하고 손쓸 방법이 없으면
170
- * 알려 뜻이 없다. 터무니없는 값은 받는다(100 미만·200만 초과).
171
- */
172
- export function 훑기상한(env = process.env) {
173
- const n = Number(env.DEEL_WALK_LIMIT);
174
- if (!Number.isFinite(n) || n < 100 || n > 2000000) return 기본훑기상한;
175
- return Math.floor(n);
176
- }
177
-
178
- /**
179
- * 폴더를 훑는다.
180
- *
181
- * @param {AbortSignal|null} signal 멈추라면 훑다 말고 나온다.
182
- * 사내망 드라이브에서는 파일 20,000개 훑기가 몇 초다. 그 사이에
183
- * ESC 눌렀는데 끝까지 훑고 나서 멈추면, 사람 눈에는 멈추지
184
- * 않는 것으로 보인다. 나온 것에 `끊김` 달아 부르는 쪽이 알게 한다
185
- * 조용히 적게 돌려주면 「그런 파일이 없다」가 되어 버린다.
186
- */
187
- export function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS, ignore = true, signal = null } = {}) {
188
- const out = [];
189
- const 건너뜀 = { 폴더: 0, 파일: 0 };
190
- let 끊김 = false;
191
- const stack = [{ dir: root, rel: '', 규칙: ignore ? 뿌리규칙읽기(root) : [] }];
192
- while (stack.length && out.length < limit) {
193
- if (signal?.aborted) { 끊김 = true; break; }
194
- const { dir, rel, 규칙 } = stack.pop();
195
- let entries;
196
- try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
197
- // 폴더에 .gitignore 있으면 아래에만 더한다 (뿌리 것은 위에서 읽었다).
198
- let 여기규칙 = 규칙;
199
- if (ignore && rel && entries.some((e) => e.name === '.gitignore' && e.isFile())) {
200
- const 추가 = 파일규칙읽기(join(dir, '.gitignore'), rel);
201
- if (추가.length) 여기규칙 = [...규칙, ...추가];
202
- }
203
- for (const e of entries) {
204
- const full = join(dir, e.name);
205
- const erel = rel ? `${rel}/${e.name}` : e.name;
206
- if (e.isDirectory()) {
207
- if (skipDirs.has(e.name)) continue;
208
- if (여기규칙.length && 걸리나(erel, true, 여기규칙)) { 건너뜀.폴더 += 1; continue; }
209
- stack.push({ dir: full, rel: erel, 규칙: 여기규칙 });
210
- } else if (e.isFile()) {
211
- if (여기규칙.length && 걸리나(erel, false, 여기규칙)) { 건너뜀.파일 += 1; continue; }
212
- let st;
213
- try { st = statSync(full); } catch { continue; }
214
- out.push({ path: full, rel: erel, mtime: st.mtimeMs, size: st.size });
215
- if (out.length >= limit) break;
216
- }
217
- }
218
- }
219
- Object.defineProperty(out, '건너뜀', { value: 건너뜀, enumerable: false });
220
- // 상한까지 찼으면 "여기서 멈췄다" 표시한다. 딱 맞아떨어져 끝난 경우까지
221
- // 잘렸다고 하게 되지만, 그쪽으로 틀리는 편이 낫다 — 덜 봤다고 말하는 것은
222
- // 사람을 한 번 더 보게 만들 뿐이고, 다 봤다고 말하는 것은 못 보게 만든다.
223
- Object.defineProperty(out, '잘림', { value: out.length >= limit, enumerable: false });
224
- Object.defineProperty(out, '상한', { value: limit, enumerable: false });
225
- Object.defineProperty(out, '끊김', { value: 끊김, enumerable: false });
226
- return out;
227
- }
228
-
229
- /**
230
- * 파일을 읽는다. 무엇으로 쓰여 있든 알아보고 읽는다.
231
- *
232
- * 두 번째 값으로 '무엇으로 읽었는지' 를 같이 준다. 부르는 쪽이 그걸 기억해 뒀다가
233
- * 되돌려 쓸 때 같은 인코딩으로 넣어야 한다. 안 그러면 사내 CP949 문서를 한 번
234
- * 고치는 것만으로 UTF-8 바뀌어 버린다.
235
- */
236
- export function readTextFull(path) {
237
- const buf = readFileSync(path);
238
- if (looksBinary(buf)) {
239
- const err = new Error('바이너리 파일입니다 텍스트로 읽을 없습니다');
240
- err.binary = true;
241
- throw err;
242
- }
243
- const r = decode(buf);
244
- return { text: r.text, encoding: r.encoding, sure: r.sure, bom: r.bom ?? 0 };
245
- }
246
-
247
- /** 글만 필요할 때. 예전 부르던 자리를 그대로 두기 위해 남긴다. */
248
- export function readText(path) {
249
- return readTextFull(path).text;
250
- }
1
+ // 파일 훑기와 glob 매칭. 외부 패키지 없이 직접 구현한다.
2
+ import { readdirSync, statSync, readFileSync, mkdirSync, copyFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { decode, looksBinary } from './encoding.js';
5
+ import { 뿌리규칙읽기, 파일규칙읽기, 걸리나 } from './ignore.js';
6
+
7
+ /**
8
+ * 폴더를 통째로 옮겨 담는다.
9
+ *
10
+ * fs.cpSync 를 안 쓰는 이유가 둘이다.
11
+ *
12
+ * 1) Node 가 실험 기능으로 표시한 API 다. 판마다 동작이 다르고, 윈도우에서
13
+ * 프로세스가 통째로 죽는 것을 실제로 겪었다 — 검사가 아무 말도 없이
14
+ * 0xC0000409 로 끝났다. 배포되는 코드가 실험 API 에 매달려 있으면 안 된다.
15
+ *
16
+ * 2) cpSync 는 심볼릭 링크를 따라간다. 남이 준 플러그인 폴더에 바깥을 가리키는
17
+ * 링크가 하나 있으면 그것까지 딸려 들어온다. 여기서는 링크를 건너뛴다 —
18
+ * 플러그인은 제 폴더 안의 글 파일이면 충분하다.
19
+ *
20
+ * 하는 일이 뻔해서 읽으면 다 보인다. 그게 이 프로젝트가 원하는 것이다.
21
+ */
22
+ export function copyDir(from, to, { skipped = [] } = {}) {
23
+ mkdirSync(to, { recursive: true });
24
+ for (const e of readdirSync(from, { withFileTypes: true })) {
25
+ const s = join(from, e.name);
26
+ const d = join(to, e.name);
27
+ if (e.isSymbolicLink()) { skipped.push(s); continue; }
28
+ if (e.isDirectory()) copyDir(s, d, { skipped });
29
+ else if (e.isFile()) copyFileSync(s, d);
30
+ // 그 밖(장치·소켓 같은 것)은 건너뛴다. 플러그인에 있을 이유가 없다.
31
+ else skipped.push(s);
32
+ }
33
+ return { skipped };
34
+ }
35
+
36
+ /**
37
+ * 다른 코딩 도구들이 제 살림을 넣어 두는 자리.
38
+ *
39
+ * 프로젝트 파일이 아니라 그 도구의 기록이다 — 지난 대화, 명령 이력, 캐시,
40
+ * 그리고 열쇠. 이 작업과 아무 상관이 없는데 훑을 때 걸려 나오면 모델이
41
+ * 그것부터 읽는다. 실제로 .claude/history.jsonl 을 읽어 컨텍스트를 채운 적이 있다.
42
+ *
43
+ * **목록은 여기 한 곳에만 둔다.** 전에는 훑는 쪽과 읽기 막는 쪽에 따로 적어
44
+ * 놨는데, 그러면 한쪽에만 새 이름을 넣는 날이 반드시 온다. 훑을 때는 안 걸리는데
45
+ * 이름을 대면 읽히는, 설명하기 어려운 상태가 된다.
46
+ *
47
+ * 새 도구는 계속 나온다. 여기 없는 이름이 보이면 그냥 한 줄 더하면 된다.
48
+ */
49
+ export const 남의도구살림 = new Set([
50
+ '.claude', '.codex', '.cursor', '.gemini', '.aider', '.continue', '.cline',
51
+ '.roo', '.kilocode', '.windsurf', '.opencode', '.zed', '.trae', '.augment',
52
+ '.qodo', '.tabnine', '.cody', '.sourcegraph', '.copilot', '.amazonq', '.junie',
53
+ '.codeium', '.goose', '.crush', '.gptme', '.openhands', '.devin',
54
+ ]);
55
+
56
+ // .aider.chat.history.md 처럼 폴더가 아니라 파일로 흘리는 것들도 있다.
57
+ export const 남의도구파일 = /^\.(aider|copilot|continue|cline|codeium|windsurf)[.-]/i;
58
+
59
+ export const SKIP_DIRS = new Set([
60
+ 'node_modules', '.git', '.deel', '.svn', '.hg', 'dist', 'build',
61
+ '.next', '.nuxt', '.cache', '__pycache__', '.venv', 'venv', 'target',
62
+ ...남의도구살림,
63
+ ]);
64
+
65
+ /**
66
+ * 이 파일이 '읽어 봐야 도움이 안 되는 살림' 인가.
67
+ *
68
+ * 왜 막나:
69
+ * 실제로 이런 일이 있었다 —
70
+ * ◧ Read(~/.deel/audit.jsonl) 77줄
71
+ * ◧ Read(~/.claude/history.jsonl) 35줄
72
+ * 감사기록은 이 프로그램이 방금 무엇을 했는지 적어 둔 것이다. 그걸 다시 읽어
73
+ * 대화에 넣으면 모델이 제 그림자를 좇는다. 사용자가 시킨 일과는 아무 상관이
74
+ * 없고, 컨텍스트만 찬다.
75
+ *
76
+ * 설정 파일은 더 나쁘다. 게이트웨이 열쇠(apiKey)가 그 안에 있다.
77
+ * 읽는 순간 그 열쇠가 대화에 실려 모델로 나가고, 세션 기록으로 디스크에도 남는다.
78
+ * 열쇠를 그 열쇠의 주인에게 보내는 셈이다.
79
+ *
80
+ * 막는 것이지 숨기는 것이 아니다 — 왜 안 되는지 그대로 말해 준다.
81
+ * @returns {string|null} 막을 이유. 막을 것이 아니면 null.
82
+ */
83
+ export function 내부살림(abs) {
84
+ const 조각 = String(abs ?? '').replace(/\\/g, '/').split('/');
85
+ const 이름 = 조각[조각.length - 1] ?? '';
86
+ const i = 조각.lastIndexOf('.deel');
87
+ if (i >= 0) {
88
+ const 안 = 조각.slice(i + 1);
89
+ if (안[0] === 'config.json') {
90
+ return 'deel 자신의 설정 파일입니다. 게이트웨이 열쇠가 들어 있어 읽지 않습니다.'
91
+ + ' 연결 상태가 궁금하면 사용자에게 /status 를 쳐 보라고 하세요.';
92
+ }
93
+ /*
94
+ * mcp.json **열쇠를 담는다.**
95
+ *
96
+ * 붙인 MCP 서버마다 `env` 를 통째로 적어 두는 자리라(backend/mcp.js),
97
+ * 토큰이 그대로 들어 있는 경우가 흔하다. config.json 만 막고 이걸 안
98
+ * 막으면 막은 것이 아니다 여기가 config.json **같은 종류의 파일**인데
99
+ * 목록에만 올라와 있었다.
100
+ *
101
+ * 목록으로 막는 방식이 이렇게 샌다. 살림 파일을 늘릴 때는 여기를
102
+ * 같이 봐야 한다 (검사: test/mention.test.js).
103
+ */
104
+ if (안[0] === 'mcp.json') {
105
+ return 'MCP 서버 설정 파일입니다. 붙여 둔 서버의 환경변수(열쇠·토큰)가 들어 있어 읽지 않습니다.'
106
+ + ' 어떤 서버가 붙어 있는지가 궁금하면 사용자에게 /mcp 쳐 보라고 하세요.';
107
+ }
108
+ if (['audit.jsonl', 'sessions', 'history'].includes(안[0])) {
109
+ return 'deel 자신의 기록입니다(무엇을 했는지 적어 둔 것). 지금 하는 일에 도움이 안 되고'
110
+ + ' 컨텍스트만 차서 읽지 않습니다. 필요한 것은 대화에 이미 다 있습니다.';
111
+ }
112
+ }
113
+ // 남의 도구 살림. 목록은 남의도구살림 한 곳에만 있다 — 훑는 쪽과 같은 것을 본다.
114
+ const 걸린것 = 조각.find((seg) => 남의도구살림.has(seg)) ?? (남의도구파일.test(이름) ? 이름 : null);
115
+ if (걸린것) {
116
+ return `${걸린것} 다른 코딩 도구가 제 기록을 넣어 두는 자리입니다.`
117
+ + ' 지난 대화·명령 이력·열쇠 같은 것이라 이 작업과 상관이 없고, 읽으면 컨텍스트만 찹니다.'
118
+ + ' 정말 그 안의 내용이 필요하면 사용자에게 직접 물어보세요.';
119
+ }
120
+ if (이름 === 'audit.jsonl') {
121
+ return 'deel 자신의 기록입니다. 읽어도 지금 하는 일에 도움이 안 됩니다.';
122
+ }
123
+ return null;
124
+ }
125
+
126
+ // glob 정규식으로. ** * ? {a,b} [abc] 를 지원한다.
127
+ export function globToRegex(pattern) {
128
+ let re = '';
129
+ let i = 0;
130
+ const p = pattern.replace(/\\/g, '/');
131
+ while (i < p.length) {
132
+ const ch = p[i];
133
+ if (ch === '*') {
134
+ if (p[i + 1] === '*') {
135
+ // **/ "0 이상의 폴더", ** "무엇이든"
136
+ if (p[i + 2] === '/') { re += '(?:[^/]*\\/)*'; i += 3; }
137
+ else { re += '.*'; i += 2; }
138
+ } else { re += '[^/]*'; i += 1; }
139
+ } else if (ch === '?') { re += '[^/]'; i += 1; }
140
+ else if (ch === '{') {
141
+ const end = p.indexOf('}', i);
142
+ if (end < 0) { re += '\\{'; i += 1; }
143
+ else {
144
+ const parts = p.slice(i + 1, end).split(',').map(escapeLiteral);
145
+ re += `(?:${parts.join('|')})`;
146
+ i = end + 1;
147
+ }
148
+ } else if (ch === '[') {
149
+ const end = p.indexOf(']', i);
150
+ if (end < 0) { re += '\\['; i += 1; }
151
+ else { re += p.slice(i, end + 1); i = end + 1; }
152
+ } else { re += escapeLiteral(ch); i += 1; }
153
+ }
154
+ return new RegExp(`^${re}$`, process.platform === 'win32' ? 'i' : '');
155
+ }
156
+
157
+ function escapeLiteral(s) {
158
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
159
+ }
160
+
161
+ /**
162
+ * 폴더를 훑어 파일 목록을 낸다. { path(절대), rel(/구분), mtime, size }
163
+ *
164
+ * `ignore` 가 켜져 있으면(기본) .gitignore 가 건너뛰라는 것은 건너뛴다 (tools/ignore.js).
165
+ * skipDirs 는 그 아래의 바닥이다 — .gitignore 가 없어도 node_modules 는 늘 건너뛴다.
166
+ * 돌려주는 배열에는 `건너뜀 = { 폴더, 파일 }` 이 (열거되지 않게) 붙어 있다. 부르는 쪽이 그 수를
167
+ * 화면에 적는다 — 조용히 빼면 "그 파일이 없다" 로 읽힌다.
168
+ * 폴더를 통째로 옮기거나 복사할 때는 `ignore: false` 그때는 다 있어야 한다.
169
+ *
170
+ * `잘림` 같이 붙는다 **상한에서 멈췄다는 뜻이다.**
171
+ *
172
+ * 전에는 상한이 조용했다. 파일 5만 개짜리 저장소에서 앞의 2만 개만 훑고
173
+ * 멈추는데, 아무 데도 그 말이 안 나갔다. 그래서 `Grep` 이 "일치 없음" 이라고
174
+ * 답했다 실제로는 뒤쪽 3만 개에 100군데가 있는데도. 찾은 것과 본 것은
175
+ * 다르고, 그걸 안 가르면 사람은 없는 줄 알고 그냥 간다. 조용한 상한은
176
+ * 틀린 답보다 나쁘다.
177
+ */
178
+ export const 기본훑기상한 = 20000;
179
+
180
+ /**
181
+ * 개까지 훑을까.
182
+ *
183
+ * 2만 개는 대부분의 저장소에 넉넉하지만 단일 저장소에는 모자란다. 그래서
184
+ * 올릴 있게 열어 둔다 보는 것을 알려 주기만 하고 손쓸 방법이 없으면
185
+ * 알려 뜻이 없다. 터무니없는 값은 안 받는다(100 미만·200만 초과).
186
+ */
187
+ export function 훑기상한(env = process.env) {
188
+ const n = Number(env.DEEL_WALK_LIMIT);
189
+ if (!Number.isFinite(n) || n < 100 || n > 2000000) return 기본훑기상한;
190
+ return Math.floor(n);
191
+ }
192
+
193
+ /**
194
+ * 폴더를 훑는다.
195
+ *
196
+ * @param {AbortSignal|null} signal 멈추라면 훑다 말고 나온다.
197
+ * 사내망 드라이브에서는 파일 20,000개 훑기가 초다. 사이에
198
+ * ESC 눌렀는데 끝까지 다 훑고 나서 멈추면, 사람 눈에는 멈추지
199
+ * 않는 것으로 보인다. 나온 것에 `끊김` 달아 부르는 쪽이 알게 한다 —
200
+ * 조용히 적게 돌려주면 「그런 파일이 없다」가 되어 버린다.
201
+ */
202
+ export function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS, ignore = true, signal = null } = {}) {
203
+ const out = [];
204
+ const 건너뜀 = { 폴더: 0, 파일: 0 };
205
+ let 끊김 = false;
206
+ const stack = [{ dir: root, rel: '', 규칙: ignore ? 뿌리규칙읽기(root) : [] }];
207
+ while (stack.length && out.length < limit) {
208
+ if (signal?.aborted) { 끊김 = true; break; }
209
+ const { dir, rel, 규칙 } = stack.pop();
210
+ let entries;
211
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
212
+ // 이 폴더에 .gitignore 가 있으면 그 아래에만 더한다 (뿌리 것은 위에서 읽었다).
213
+ let 여기규칙 = 규칙;
214
+ if (ignore && rel && entries.some((e) => e.name === '.gitignore' && e.isFile())) {
215
+ const 추가 = 파일규칙읽기(join(dir, '.gitignore'), rel);
216
+ if (추가.length) 여기규칙 = [...규칙, ...추가];
217
+ }
218
+ for (const e of entries) {
219
+ const full = join(dir, e.name);
220
+ const erel = rel ? `${rel}/${e.name}` : e.name;
221
+ if (e.isDirectory()) {
222
+ if (skipDirs.has(e.name)) continue;
223
+ if (여기규칙.length && 걸리나(erel, true, 여기규칙)) { 건너뜀.폴더 += 1; continue; }
224
+ stack.push({ dir: full, rel: erel, 규칙: 여기규칙 });
225
+ } else if (e.isFile()) {
226
+ if (여기규칙.length && 걸리나(erel, false, 여기규칙)) { 건너뜀.파일 += 1; continue; }
227
+ let st;
228
+ try { st = statSync(full); } catch { continue; }
229
+ out.push({ path: full, rel: erel, mtime: st.mtimeMs, size: st.size });
230
+ if (out.length >= limit) break;
231
+ }
232
+ }
233
+ }
234
+ Object.defineProperty(out, '건너뜀', { value: 건너뜀, enumerable: false });
235
+ // 상한까지 찼으면 "여기서 멈췄다" 고 표시한다. 딱 맞아떨어져 끝난 경우까지
236
+ // 잘렸다고 하게 되지만, 그쪽으로 틀리는 편이 낫다 — 덜 봤다고 말하는 것은
237
+ // 사람을 번 더 보게 만들 뿐이고, 다 봤다고 말하는 것은 못 보게 만든다.
238
+ Object.defineProperty(out, '잘림', { value: out.length >= limit, enumerable: false });
239
+ Object.defineProperty(out, '상한', { value: limit, enumerable: false });
240
+ Object.defineProperty(out, '끊김', { value: 끊김, enumerable: false });
241
+ return out;
242
+ }
243
+
244
+ /**
245
+ * 글 파일을 읽는다. 무엇으로 쓰여 있든 알아보고 읽는다.
246
+ *
247
+ * 번째 값으로 '무엇으로 읽었는지' 같이 준다. 부르는 쪽이 그걸 기억해 뒀다가
248
+ * 되돌려 쓸 때 같은 인코딩으로 넣어야 한다. 안 그러면 사내 CP949 문서를 한 번
249
+ * 고치는 것만으로 UTF-8 로 바뀌어 버린다.
250
+ */
251
+ export function readTextFull(path) {
252
+ const buf = readFileSync(path);
253
+ if (looksBinary(buf)) {
254
+ const err = new Error('바이너리 파일입니다 — 텍스트로 읽을 수 없습니다');
255
+ err.binary = true;
256
+ throw err;
257
+ }
258
+ const r = decode(buf);
259
+ return { text: r.text, encoding: r.encoding, sure: r.sure, bom: r.bom ?? 0 };
260
+ }
261
+
262
+ /** 글만 필요할 때. 예전 부르던 자리를 그대로 두기 위해 남긴다. */
263
+ export function readText(path) {
264
+ return readTextFull(path).text;
265
+ }