deel-local-cli 1.1.1 → 1.4.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 (60) hide show
  1. package/README.en.md +826 -19
  2. package/README.md +689 -14
  3. package/bin/deel.js +73 -4
  4. package/package.json +7 -3
  5. package/src/acp/jsonrpc.js +230 -0
  6. package/src/acp/map.js +219 -0
  7. package/src/acp/serve.js +556 -0
  8. package/src/agent/budget.js +0 -14
  9. package/src/agent/card.js +110 -0
  10. package/src/agent/compact.js +95 -0
  11. package/src/agent/evidence.js +186 -0
  12. package/src/agent/evolve.js +213 -0
  13. package/src/agent/grade.js +20 -0
  14. package/src/agent/loop.js +172 -14
  15. package/src/agent/models.js +169 -0
  16. package/src/agent/modes.js +49 -1
  17. package/src/agent/pins.js +140 -0
  18. package/src/agent/project.js +10 -4
  19. package/src/agent/session.js +359 -11
  20. package/src/agent/store.js +31 -0
  21. package/src/agent/threads.js +154 -0
  22. package/src/commands.js +751 -55
  23. package/src/config.js +10 -1
  24. package/src/i18n/en.js +244 -0
  25. package/src/i18n/index.js +126 -0
  26. package/src/i18n/ko.js +231 -0
  27. package/src/lsp/client.js +459 -0
  28. package/src/lsp/diag.js +112 -0
  29. package/src/lsp/rpc.js +84 -0
  30. package/src/lsp/servers.js +218 -0
  31. package/src/oneshot.js +23 -0
  32. package/src/pack/sbom.js +218 -0
  33. package/src/pack/selfpack.js +19 -3
  34. package/src/repl.js +273 -22
  35. package/src/safety/guard.js +37 -2
  36. package/src/safety/secrets.js +205 -0
  37. package/src/safety/undo.js +11 -4
  38. package/src/setup.js +2 -2
  39. package/src/skills/discover.js +1 -1
  40. package/src/tools/desc.en.js +219 -0
  41. package/src/tools/edit-match.js +16 -5
  42. package/src/tools/excel-com.js +1 -1
  43. package/src/tools/excel.js +1 -1
  44. package/src/tools/fsutil.js +0 -8
  45. package/src/tools/index.js +149 -6
  46. package/src/tools/lsp.js +327 -0
  47. package/src/tools/task.js +30 -2
  48. package/src/tools/todo.js +19 -3
  49. package/src/ui/ansi.js +42 -2
  50. package/src/ui/approve.js +25 -21
  51. package/src/ui/inputbox.js +69 -13
  52. package/src/ui/intro.js +174 -0
  53. package/src/ui/level.js +11 -5
  54. package/src/ui/md.js +227 -0
  55. package/src/ui/notify.js +101 -0
  56. package/src/ui/prompt.js +1 -1
  57. package/src/ui/screen.js +1 -1
  58. package/src/ui/status.js +190 -34
  59. package/src/ui/working.js +0 -3
  60. package/src/ui/wrap.js +1 -8
@@ -0,0 +1,205 @@
1
+ // 비밀이 모델에게, 그리고 디스크에 남는 대화 기록에 끼어드는 것을 막는다.
2
+ //
3
+ // ── 어디서 새나 ────────────────────────────────────────────────────────
4
+ //
5
+ // 사람이 열쇠를 붙여 넣는 일은 드물다. 새는 자리는 거의 항상 **명령 출력**이다.
6
+ //
7
+ // env · printenv · set 환경변수를 통째로 찍는다
8
+ // git remote -v https://사람:토큰@github.com/... 이 그대로
9
+ // docker inspect · kubectl get -o 설정에 박힌 열쇠가 그대로
10
+ // curl -v Authorization 헤더가 그대로
11
+ // 검사 실패 로그 연결 문자열이 통째로
12
+ //
13
+ // 이게 왜 문제인가. 그 글은 모델에게 실려 가고 — 사내 게이트웨이라 해도
14
+ // 그쪽 로그에 남는다 — 동시에 `.deel/sessions/*.jsonl` 로 디스크에 적힌다.
15
+ // 그 파일은 나중에 `/recall` 로 다시 읽히고, `deel pack` 에 딸려 갈 수도 있다.
16
+ // 한 번 새면 여러 벌이 된다.
17
+ //
18
+ // ── 파일 내용은 안 가린다 ───────────────────────────────────────────────
19
+ //
20
+ // 여기서 제일 중요한 결정이다. **읽은 파일은 손대지 않는다.**
21
+ //
22
+ // 가리고 싶은 마음이 드는 자리가 바로 `.env` 인데, 거기를 가리면 이렇게 된다 —
23
+ // 모델이 가려진 글을 보고, 그걸 고쳐서 Write 로 되돌려 쓴다. 그러면 진짜
24
+ // 열쇠가 있던 자리에 `«가림»` 이 적힌다. **사람의 열쇠가 우리 손에 지워진다.**
25
+ // 비밀을 지키려다 비밀을 파괴하는 셈이다.
26
+ //
27
+ // 그래서 파일 쪽은 가리는 대신 **알린다** — 감사기록에 남기고 화면에 띄운다.
28
+ // 무엇을 못 막는지 분명히 말하는 편이, 막았다고 해 놓고 파일을 망가뜨리는 것보다
29
+ // 언제나 낫다.
30
+ //
31
+ // 가리는 것은 **한 번 쓰고 버려지는 출력**뿐이다. 명령 출력·웹에서 받아온 글은
32
+ // 모델이 그대로 되돌려 쓸 일이 없다.
33
+
34
+ /** 가려진 자리에 남는 표. 종류를 같이 적어 무엇이 가려졌는지 알 수 있게 한다. */
35
+ const 표 = (종류) => `«가림:${종류}»`;
36
+
37
+ /**
38
+ * 찾는 것들.
39
+ *
40
+ * 순서가 뜻을 갖는다 — 좁은 것을 먼저 본다. `sk-ant-...` 를 `sk-...` 가 먼저
41
+ * 집어삼키면 무엇이 가려졌는지가 뭉개진다.
42
+ *
43
+ * 넓게 잡지 않는다. 코드에 흔한 글자를 비밀로 오인해 가려 버리면, 모델이 보는
44
+ * 코드가 조용히 달라진다. 그건 여기서 막으려는 것보다 더 나쁜 고장이다.
45
+ */
46
+ export const 갈래 = [
47
+ {
48
+ id: '사설키',
49
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
50
+ },
51
+ { id: 'anthropic', re: /\bsk-ant-[A-Za-z0-9_-]{16,}/g },
52
+ { id: 'openai', re: /\bsk-[A-Za-z0-9_-]{20,}/g },
53
+ { id: 'github', re: /\b(?:gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,})\b/g },
54
+ { id: 'slack', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/g },
55
+ { id: 'aws', re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
56
+ { id: 'google', re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
57
+ {
58
+ // 머리.몸통.서명 세 토막. 안에 무엇이 들었는지 우리는 안 열어 본다.
59
+ id: 'jwt',
60
+ re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
61
+ },
62
+ {
63
+ // https://사람:토큰@호스트 — git remote -v 가 이 모양으로 찍는다.
64
+ // 사람 이름은 남긴다. 무슨 계정인지 모르면 사람이 손을 못 쓴다.
65
+ id: '주소속열쇠',
66
+ re: /([a-z][a-z0-9+.-]*:\/\/)([^\s/:@]{1,64}):([^\s/@]{1,256})@/gi,
67
+ 바꾸기: (m, 앞, 사람) => `${앞}${사람}:${표('주소속열쇠')}@`,
68
+ },
69
+ {
70
+ /*
71
+ * 줄 끝까지 가린다.
72
+ *
73
+ * `Authorization: Bearer eyJ…` 에서 `\S+` 만 잡으면 'Bearer' 만 가려지고
74
+ * 진짜 값은 그 뒤에 그대로 남는다. 앞의 낱말을 가리고 뒤의 열쇠를 남기는
75
+ * 것은 안 가린 것만 못하다 — 가렸다고 믿게 만들기 때문이다.
76
+ *
77
+ * 이미 가린 자리는 다시 안 건드린다. 두 번 가리면 무엇이 가려졌는지가
78
+ * 뭉개지고 셈도 부풀려진다.
79
+ */
80
+ id: '헤더',
81
+ re: /\b(Authorization|Proxy-Authorization|X-Api-Key|Api-Key|X-Auth-Token)(\s*[:=]\s*)(?!«)([^\r\n]+)/gi,
82
+ 바꾸기: (m, 이름, 사이) => `${이름}${사이}${표('헤더')}`,
83
+ },
84
+ {
85
+ /*
86
+ * 이름이 열쇠라고 말하는 환경변수.
87
+ *
88
+ * 값이 비었거나 이미 가려진 것은 그냥 둔다 — `API_KEY=` 만 있는 줄까지
89
+ * 손대면 화면에 «가림» 만 늘어나고 알아볼 것이 없어진다.
90
+ */
91
+ id: '환경변수',
92
+ re: /\b([A-Z][A-Z0-9_]{2,}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIALS?))(\s*[:=]\s*)(["']?)(?!«)([^\s"']{4,})\3/g,
93
+ 바꾸기: (m, 이름, 사이) => `${이름}${사이}${표('환경변수')}`,
94
+ },
95
+ ];
96
+
97
+ /**
98
+ * 안 고치고 세기만 한다.
99
+ *
100
+ * 파일을 읽었을 때 쓰는 길이다. 무엇이 들어왔는지는 알려야 하고, 글자는
101
+ * 건드리면 안 된다.
102
+ *
103
+ * @returns {Array<{종류: string, 몇번: number}>}
104
+ */
105
+ export function 훑기(글, { 열쇠들 = [] } = {}) {
106
+ /*
107
+ * 가려 보고 그 결과만 센다. 글은 안 돌려준다.
108
+ *
109
+ * 세는 길을 따로 두면 두 벌이 되고, 두 벌은 언젠가 어긋난다 — "가릴 때는
110
+ * 3군데였는데 셀 때는 5군데" 같은 식으로. 겹치는 규칙 때문에 실제로 그렇게
111
+ * 된다. 한 길로만 간다.
112
+ */
113
+ return 가리기(글, { 열쇠들 }).가린것;
114
+ }
115
+
116
+ /**
117
+ * 가린다.
118
+ *
119
+ * @param {string} 글
120
+ * @param {object} o
121
+ * @param {string[]} o.열쇠들 정확히 아는 비밀 (설정에 든 게이트웨이 열쇠 등).
122
+ * 이건 짐작이 아니라 아는 값이라 언제나 먼저 지운다.
123
+ * @returns {{글: string, 가린것: Array<{종류, 몇번}>}}
124
+ */
125
+ export function 가리기(글, { 열쇠들 = [] } = {}) {
126
+ const 원본 = String(글 ?? '');
127
+ if (!원본) return { 글: 원본, 가린것: [] };
128
+
129
+ let s = 원본;
130
+
131
+ // 아는 값부터. 짐작보다 아는 것이 먼저다.
132
+ for (const 열쇠 of 아는열쇠(열쇠들)) {
133
+ if (s.includes(열쇠)) s = s.split(열쇠).join(표('설정한열쇠'));
134
+ }
135
+
136
+ for (const g of 갈래) {
137
+ g.re.lastIndex = 0;
138
+ s = s.replace(g.re, (...인자) => (g.바꾸기 ? g.바꾸기(...인자) : 표(g.id)));
139
+ }
140
+
141
+ return { 글: s, 가린것: 남은표세기(s) };
142
+ }
143
+
144
+ /*
145
+ * 몇 군데를 가렸는지는 **결과를 보고** 센다. 바꾼 횟수를 세면 안 된다.
146
+ *
147
+ * 규칙끼리 겹치기 때문이다 — `Authorization: Bearer eyJ…` 는 jwt 가 먼저
148
+ * 가리고 헤더가 그 위를 다시 가린다. 바꾼 횟수로 세면 "2군데를 가렸습니다"
149
+ * 가 되는데 글에는 표가 하나뿐이다. 사람이 세어 보면 안 맞는다.
150
+ */
151
+ function 남은표세기(글) {
152
+ const 센것 = new Map();
153
+ for (const m of String(글).matchAll(/«가림:([^»]+)»/g)) {
154
+ 센것.set(m[1], (센것.get(m[1]) ?? 0) + 1);
155
+ }
156
+ return [...센것].map(([종류, 몇번]) => ({ 종류, 몇번 }));
157
+ }
158
+
159
+ /**
160
+ * 모델에게 붙일 한마디.
161
+ *
162
+ * 표만 남기고 말이 없으면 모델은 «가림:openai» 를 진짜 값으로 알고 그걸
163
+ * 명령에 다시 써 넣는다. 그러면 명령이 실패하고, 왜 실패했는지 모른 채
164
+ * 같은 것을 되풀이한다. 한 줄이면 그 고리가 안 생긴다.
165
+ */
166
+ export function 가렸다는말(가린것) {
167
+ if (!가린것?.length) return '';
168
+ const 셈 = 가린것.reduce((a, x) => a + x.몇번, 0);
169
+ const 종류 = 가린것.map((x) => x.종류).join(' · ');
170
+ return `\n\n(deel 이 이 출력에서 비밀로 보이는 값 ${셈}군데를 가렸습니다: ${종류}.`
171
+ + ' 「«가림:…»」 은 진짜 값이 아닙니다 — 명령에 그대로 써 넣지 마세요.'
172
+ + ' 그 값이 꼭 필요하면 사용자에게 물어보세요.)';
173
+ }
174
+
175
+ /** 사람에게 보일 한 줄. 파일 쪽에서는 이것만 하고 글은 안 건드린다. */
176
+ export function 봤다는말(가린것) {
177
+ if (!가린것?.length) return '';
178
+ const 셈 = 가린것.reduce((a, x) => a + x.몇번, 0);
179
+ return `비밀로 보이는 값 ${셈}군데가 대화에 들어갔습니다 (${가린것.map((x) => x.종류).join(' · ')})`;
180
+ }
181
+
182
+ /**
183
+ * 지금 설정에서 '정확히 아는 비밀' 을 모은다.
184
+ *
185
+ * 짧은 것은 뺀다. 세 글자짜리 열쇠를 그대로 찾아 지우면 멀쩡한 글에서
186
+ * 그 세 글자가 든 자리가 전부 뭉개진다 — 열쇠가 'test' 인 시험 설정에서
187
+ * 실제로 그럴 수 있다.
188
+ */
189
+ export function 아는열쇠(것들) {
190
+ const out = [];
191
+ for (const v of Array.isArray(것들) ? 것들 : [것들]) {
192
+ const s = typeof v === 'string' ? v.trim() : '';
193
+ if (s.length >= 8 && !out.includes(s)) out.push(s);
194
+ }
195
+ // 긴 것부터 지운다. 짧은 것이 긴 것의 일부일 때 순서가 뒤바뀌면 반쪽만 지워진다.
196
+ return out.sort((a, b) => b.length - a.length);
197
+ }
198
+
199
+ /**
200
+ * 이 도구의 결과를 가려도 되는가.
201
+ *
202
+ * 한 번 쓰고 버려지는 출력만 가린다. 파일에서 읽어 온 글은 모델이 그대로
203
+ * 되돌려 쓸 수 있으므로 절대 안 가린다 — 위 머리말 참고.
204
+ */
205
+ export const 가릴도구 = new Set(['Bash', 'Verify', 'Jobs', 'WebFetch']);
@@ -1,7 +1,7 @@
1
1
  // 되돌리기. 승인 프롬프트를 안 쓰는 대신 이게 안전망이다.
2
2
  // 파일을 고치기 전에 항상 이전 내용을 떠 놓고, /undo 로 턴 단위로 되돌린다.
3
3
  import { join } from 'node:path';
4
- import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync, readdirSync, statSync } from 'node:fs';
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync, statSync } from 'node:fs';
5
5
  import { looksBinary } from '../tools/encoding.js';
6
6
 
7
7
  // 되돌리기 이력은 파일 내용을 통째로 담는다. 이만큼 커지면 오래된 턴을 버린다.
@@ -131,11 +131,18 @@ export class History {
131
131
  return seen;
132
132
  }
133
133
 
134
- // 최근 n 개 턴을 되돌린다. 되돌린 파일 목록을 반환.
134
+ /**
135
+ * 최근 n 개 턴을 되돌린다. 되돌린 파일 목록을 반환.
136
+ *
137
+ * turnIds 를 같이 준다 — **어느** 턴을 되돌렸는지 부르는 쪽이 알아야 한다.
138
+ * 파일만 되돌리고 대화는 그대로 두면, 모델은 지워진 코드가 아직 있는 줄 알고
139
+ * 그 위에 이어서 일한다. 그 거짓말을 걷어내려면 개수가 아니라 번호가 필요하다.
140
+ * (session.되감기 를 볼 것.)
141
+ */
135
142
  undo(n = 1) {
136
143
  const recs = this.all();
137
144
  const turns = this.turns().slice(-n);
138
- if (!turns.length) return { restored: [], turns: 0 };
145
+ if (!turns.length) return { restored: [], turns: 0, turnIds: [] };
139
146
 
140
147
  const target = recs.filter((r) => turns.includes(r.turn));
141
148
  // 같은 파일이 여러 번 바뀌었으면 가장 이른 상태로 되돌려야 한다.
@@ -173,7 +180,7 @@ export class History {
173
180
  // 되돌린 기록은 잘라낸다.
174
181
  const keep = recs.filter((r) => !turns.includes(r.turn));
175
182
  writeFileSync(this.file, keep.map((r) => JSON.stringify(r)).join('\n') + (keep.length ? '\n' : ''), 'utf8');
176
- return { restored, turns: turns.length };
183
+ return { restored, turns: turns.length, turnIds: turns.slice() };
177
184
  }
178
185
  }
179
186
 
package/src/setup.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // 첫 실행 마법사 + 진단 실행.
2
- import { c, say, rule, mark, width } from './ui/ansi.js';
3
- import { ask, pick, confirm } from './ui/prompt.js';
2
+ import { c, say, rule, mark } from './ui/ansi.js';
3
+ import { ask, pick } from './ui/prompt.js';
4
4
  import { spin } from './ui/spinner.js';
5
5
  import { detect } from './backend/detect.js';
6
6
  import { probe } from './backend/probe.js';
@@ -7,7 +7,7 @@
7
7
  // 플러그인도 없다 — 거기서는 방법론이 0개였고, 모델은 매번 제 나름대로 했다.
8
8
  // 시킨 것만 겨우 하고 끝나는 얄팍한 결과가 거기서 나온다.
9
9
  // 품고 다니는 것은 **가장 낮은 자리**에 둔다. 같은 이름을 사용자가 만들면 그쪽이 이긴다.
10
- import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
10
+ import { readdirSync, readFileSync, existsSync } from 'node:fs';
11
11
  import { join, basename, dirname } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { homedir } from 'node:os';
@@ -0,0 +1,219 @@
1
+ /**
2
+ * 도구 설명 영어판 — 모델이 읽는 글이다.
3
+ *
4
+ * ── 왜 여기 따로 두나 ───────────────────────────────────────────────────
5
+ *
6
+ * index.js 의 한글 설명은 손대지 않는다. 저 글은 오래 겪으면서 한 문장씩
7
+ * 눌러 온 것이라(“앞부분을 다시 보내지 마라”, “파일 목록만 봐서는 안 보인다”),
8
+ * 그 파일을 두 언어로 갈라 놓으면 다음에 한쪽만 고치게 된다. 여기 표에
9
+ * 없는 것은 한글 설명이 그대로 나간다 — 화면 말과 같은 규칙이다.
10
+ *
11
+ * ── 왜 옮기나 ───────────────────────────────────────────────────────────
12
+ *
13
+ * 두 가지다. 하나는 영어로 켠 사람이 한국어 도구 설명을 받으면, 모델이
14
+ * 무엇을 골라야 할지를 못 읽는다는 것. 다른 하나는 **토큰**이다.
15
+ * 도구 정의는 매 요청에 통째로 실리는 고정 몫인데, 한글은 글자당 약 1토큰이고
16
+ * 영문은 약 3.6자당 1토큰이다. 32k 창에서 이 몫이 10% 를 넘게 먹고 있었다.
17
+ *
18
+ * ── 옮길 때 지킨 것 ─────────────────────────────────────────────────────
19
+ *
20
+ * 규칙을 한 줄도 안 뺐다. 특히 이런 줄들은 글자 그대로 옮겼다 —
21
+ * 빠지면 그 모델만 조용히 다르게 굴고, 그 차이는 몇 걸음 뒤에야 드러난다.
22
+ *
23
+ * Edit “먼저 Read 로 읽어야 한다” · 안 읽고 고치는 것을 막는 자리
24
+ * Append“앞부분을 다시 보내지 마라” · 같은 자리에서 또 잘리는 것을 막는 자리
25
+ * Bash “끝나지 않는 것은 background” · 시간 초과로 죽는 것을 막는 자리
26
+ * Verify“확인 못 한 것은 못 했다고” · 이 프로그램이 거짓말을 안 하게 하는 자리
27
+ *
28
+ * 도구 이름과 인자 이름은 **안 옮긴다.** 그건 식별자다. Task 의 목적·할일처럼
29
+ * 한글로 된 인자 이름도 그대로 둔다 — 이름을 바꾸면 그 도구가 아예 안 불린다.
30
+ */
31
+ export const 도구설명EN = {
32
+ Read: {
33
+ desc: 'Read one file. Line numbers come back with it. You must read a file before editing it.'
34
+ + ' Excel files (.xlsx/.xlsm/.xls) can be read directly too — they come back as CSV per sheet,'
35
+ + ' so there is no need to ask the user to export anything. Excel files are read-only here, though.',
36
+ params: {
37
+ file_path: 'path of the file to read',
38
+ offset: 'first line (1-based). Only for large files',
39
+ limit: 'how many lines to read',
40
+ },
41
+ },
42
+ Write: {
43
+ desc: 'Create a file, or overwrite one completely. Use Edit to change part of a file.'
44
+ + ' **You can create several files in one call** — pass them as an array in files.'
45
+ + ' Do that when you are laying out a folder structure. One call per file means one model'
46
+ + ' round trip per file, and an eight-file skeleton loses minutes to that.',
47
+ params: {
48
+ file_path: 'path to write (single file)',
49
+ content: 'the whole file content (single file)',
50
+ files: 'several files at once. When you use this, leave file_path and content out.',
51
+ },
52
+ },
53
+ Append: {
54
+ desc: 'Append to the end of a file. This is how you build a large file — Write the first part,'
55
+ + ' then call Append repeatedly until it is complete. Splitting it and landing it for certain'
56
+ + ' beats trying to fit it in one call and getting cut off. No Read needed first — you are only'
57
+ + ' adding to the end, so there is nothing to read.',
58
+ params: {
59
+ file_path: 'path of the file to append to',
60
+ content: 'what to add at the end',
61
+ },
62
+ },
63
+ Edit: {
64
+ desc: 'Replace an exactly matching string in a file. You must Read it first.'
65
+ + ' **If there are several places to change, send them in one call as the edits array** —'
66
+ + ' they may even be in different files. One call per place means one model round trip per'
67
+ + ' place, and a six-place cleanup loses minutes to that.',
68
+ params: {
69
+ file_path: 'path of the file to edit (single edit)',
70
+ old_string: 'what to replace. Must be unique within the file',
71
+ new_string: 'what to replace it with',
72
+ replace_all: 'true to replace every occurrence',
73
+ edits: 'several places at once, applied in the order given. When you use this, leave the arguments above out.',
74
+ },
75
+ },
76
+ Glob: {
77
+ desc: 'Find files by name pattern. e.g. **/*.js, src/**/*.{ts,tsx}',
78
+ params: {
79
+ pattern: 'glob pattern',
80
+ path: 'folder to start from. Defaults to the whole working folder',
81
+ },
82
+ },
83
+ Grep: {
84
+ desc: 'Search file contents with a regular expression.',
85
+ params: {
86
+ pattern: 'regular expression',
87
+ path: 'folder or file to search',
88
+ glob: 'restrict which files. e.g. **/*.js',
89
+ output_mode: 'defaults to files_with_matches',
90
+ '-i': 'ignore case',
91
+ '-n': 'show line numbers',
92
+ head_limit: 'cap the number of results',
93
+ },
94
+ },
95
+ Skill: {
96
+ desc: 'Open one skill and read it. The list carries only names and descriptions,'
97
+ + ' so pick the one you need and pull its body with this.',
98
+ params: { name: 'skill name, exactly as listed' },
99
+ },
100
+ Bash: {
101
+ desc: 'Run a command. Commands that cannot be undone are blocked.'
102
+ + ' **Anything that never ends (dev servers, watch) must be started with background: true** —'
103
+ + ' called plainly it dies on timeout. After starting one, read its output with Jobs.',
104
+ params: {
105
+ command: 'the command to run',
106
+ description: 'one line on what this command does',
107
+ timeout: 'time limit in ms. Default 120000',
108
+ background: 'true for a command that never ends. Returns immediately; read it with Jobs',
109
+ },
110
+ },
111
+ WebFetch: {
112
+ desc: 'Read a web page. Read-only — nothing is sent. Use it to check documentation, an error'
113
+ + ' message, or how a library is used. Addresses on this machine or an internal network are'
114
+ + ' not read. Several calls to the same site go out one after another, so they cost that much'
115
+ + ' more time — fetch only what you need. Truncated JSON cannot be read, so if it comes back'
116
+ + ' cut, narrow the request or raise max_chars and call again.',
117
+ params: {
118
+ url: 'address to read (http/https)',
119
+ max_chars: 'maximum characters to pull. Left out, it is sized to the model. Raise it if the'
120
+ + ' material comes back cut (max 120000)',
121
+ },
122
+ },
123
+ Recall: {
124
+ desc: 'Search past sessions in this folder. When the user points back ("last time"), use this'
125
+ + ' instead of asking again. This does not search file contents — that is Grep.',
126
+ params: {
127
+ query: 'what to look for. Two or three words (e.g. "CP949 encoding")',
128
+ limit: 'how many to bring back (default 8)',
129
+ tools: 'also dig through tool results (default false)',
130
+ },
131
+ },
132
+ Remember: {
133
+ desc: 'Write one line that outlives this session. Rules the user set, promises made, mistakes'
134
+ + ' not to repeat. Do not record anything that only applies to this job, or anything a file'
135
+ + ' would tell you. This line rides on every later request — keep it to one sentence.',
136
+ params: { text: 'one line (e.g. "internal documents are read as CP949 and written back as CP949")' },
137
+ },
138
+ TodoWrite: {
139
+ desc: 'Create and update the todo list. For anything that takes several steps, build the list'
140
+ + ' first and update it the moment each step finishes. Always send the whole list. state is'
141
+ + ' one of todo / doing / done, and only one item may be doing at a time. The number of steps'
142
+ + ' is set by the size of the job — there is no fixed count and no cap. Do not squeeze unrelated'
143
+ + ' work into one line to hit a number. Write each step small enough to check on its own.',
144
+ params: { todos: 'the whole todo list. No cap — as many as the job needs' },
145
+ },
146
+ Verify: {
147
+ desc: 'Check that what you made actually works. Call this **before** you finish, without fail.'
148
+ + ' A file existing and a file working are different things — an unclosed tag, a src pointing'
149
+ + ' at a file that is not there, one missing bracket in JS: none of that shows in a file listing.'
150
+ + ' What can be run gets run (node --check, py_compile); what cannot gets read (HTML tag pairs,'
151
+ + ' missing references, CSS braces, JSON). Whatever it could not check, it tells you it could not.'
152
+ + ' Running tests or a build is Bash — that goes through the user for approval.',
153
+ params: { paths: 'files to check. Left out, it checks everything checkable in the working folder.' },
154
+ },
155
+ Outline: {
156
+ desc: 'See only the **skeleton** of a folder or file — per file, the names and line numbers of'
157
+ + ' functions, classes, types, and headings. Call this before touching code you did not write.'
158
+ + ' It tells you what is where for a fraction of what reading whole files costs. Pick the places'
159
+ + ' to change here, then Read **only those files**. Reads js/ts, py, java/kotlin, go, rust, c#,'
160
+ + ' md, html, css, sh, json.',
161
+ params: {
162
+ path: 'folder or file path. Defaults to the whole working folder',
163
+ pattern: 'narrow by name (e.g. **/*.js). Left out, everything',
164
+ },
165
+ },
166
+ Task: {
167
+ desc: 'Split a chunk of a large job off as a subtask and **run it separately.** The subtask works'
168
+ + ' start to finish in its own conversation and returns only a summary — the files it read do'
169
+ + ' not pile up in your window. That is why work that creates or edits several files has to be'
170
+ + ' divided this way to get to the end. One chunk must be finishable on its own (e.g. "create'
171
+ + ' index.html and style.css"). The subtask cannot see your conversation — put everything it'
172
+ + ' needs into 할일. Do not use this for one short job. Doing it yourself is faster.',
173
+ params: {
174
+ 목적: 'this chunk in one line (e.g. "build the dashboard page skeleton")',
175
+ 할일: 'everything the subtask has to do. It cannot see this conversation, so put the background,'
176
+ + ' the decisions, and the file paths here. Say what counts as done, too.',
177
+ 모드: 'how the subtask works: code (builds and edits) · debug (finds causes) · ask (reads and'
178
+ + ' answers only). Defaults to code.',
179
+ 모델: 'hand this chunk to a **different model**. Only profile names the user has configured'
180
+ + ' work (do not invent an address — it will not be accepted). Left out, it stays on the model'
181
+ + ' you are using. Handing routine work (formatting, repetitive edits, short summaries) to a'
182
+ + ' small model keeps your window from filling. Do the work that needs judgement yourself.',
183
+ },
184
+ },
185
+ Def: {
186
+ desc: 'Ask the language server **where a name is defined.** You get the location without reading'
187
+ + ' the file. Unlike Grep it does not hand you the wrong places — not the same name in a comment,'
188
+ + ' not the same name in a third-party library, not the same name inside a string.'
189
+ + ' Call this before touching code you did not write. Once you know where it is, Read only that file.'
190
+ + ' If the name exists in several places you get the list, and file_path picks one.',
191
+ params: {
192
+ name: 'the name to find (function, class, variable)',
193
+ file_path: 'the file the name is used in. Use it when the same name exists in several places',
194
+ line: 'line number inside file_path where the name appears (1-based)',
195
+ },
196
+ },
197
+ Refs: {
198
+ desc: 'Ask the language server for **every place a name is used.** Call it before you rename'
199
+ + ' something or change a function — this is what tells you how many places have to change together.'
200
+ + ' Unlike the hundreds of lines Grep gives you, only the places that really use it come back.'
201
+ + ' Grep still finds comments, config and docs, though: use this for the code and Grep for the rest'
202
+ + ' when you rename something outright.',
203
+ params: {
204
+ name: 'the name to find (function, class, variable)',
205
+ file_path: 'the file the name is defined in. Use it when the same name exists in several places',
206
+ line: 'line number inside file_path where the name appears (1-based)',
207
+ include_declaration: 'include the definition itself. Default false',
208
+ },
209
+ },
210
+ Jobs: {
211
+ desc: 'List, read, and end background commands (Bash with background). Called with no number,'
212
+ + ' you get the list. Given a number, you get whatever output arrived since last time.'
213
+ + ' If you started a server, you must end it when the job is done.',
214
+ params: {
215
+ 번호: 'job number to look at. Left out, the list',
216
+ 끝내기: 'true to end that job (stop)',
217
+ },
218
+ },
219
+ };
@@ -46,11 +46,22 @@ function findAll(text, needle, tier) {
46
46
  let re;
47
47
  try { re = new RegExp(p, 'g'); } catch { return []; }
48
48
  const out = [];
49
- for (const m of text.matchAll(re)) {
50
- if (m[0].length === 0) continue;
51
- out.push({ start: m.index, end: m.index + m[0].length });
52
- if (out.length > 50) break; // 너무 많으면 어차피 모호하다
53
- }
49
+ // 만들 말고 **돌릴 때** 터지는 것이 있다.
50
+ //
51
+ // 모델은 파일을 고칠 덩이를 통째로 old_string 에 담아 보낸다. 4만 자쯤
52
+ // 넘어가면 new RegExp 멀쩡히 만들어지는데(패턴만 훑는다) 실제로 돌릴 때
53
+ // 프로그램 크기 한도에 걸려 SyntaxError 가 난다. 위의 try 는 만들 때만 감싸므로
54
+ // 그 오류가 그대로 튀어나가서 "찾지 못했습니다" 대신 도구가 죽었다.
55
+ //
56
+ // 여기서는 못 찾은 것으로 친다. 정확히 일치하는 경우는 위에서 indexOf 로 이미
57
+ // 처리했으니, 느슨하게 맞춰 보는 이 길만 포기하는 것이다.
58
+ try {
59
+ for (const m of text.matchAll(re)) {
60
+ if (m[0].length === 0) continue;
61
+ out.push({ start: m.index, end: m.index + m[0].length });
62
+ if (out.length > 50) break; // 너무 많으면 어차피 모호하다
63
+ }
64
+ } catch { return []; }
54
65
  return out;
55
66
  }
56
67
 
@@ -17,7 +17,7 @@
17
17
  // 2) 엑셀은 바쁘면 호출을 거절한다(0x80010001, 0x800AC472). 잘못된 호출이
18
18
  // 아니라 '지금은 말고' 라는 뜻이라, 쉬었다 다시 부르면 된다.
19
19
  import { spawn } from 'node:child_process';
20
- import { mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
20
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
21
21
  import { tmpdir } from 'node:os';
22
22
  import { join } from 'node:path';
23
23
  import { decode } from './encoding.js';
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // 부르는 쪽은 어느 갈래인지 몰라도 된다. 다만 암호가 필요할 때는 물어볼 수
7
7
  // 있어야 하므로, 물어보는 방법을 받아 온다(askPassword).
8
- import { readFileSync, statSync } from 'node:fs';
8
+ import { readFileSync } from 'node:fs';
9
9
  import { extname } from 'node:path';
10
10
  import { readXlsx, toCsv, looksXlsx, looksOle } from './xlsx.js';
11
11
  import { excelToTables, canUseExcel } from './excel-com.js';
@@ -171,14 +171,6 @@ export function walk(root, { limit = 20000, skipDirs = SKIP_DIRS } = {}) {
171
171
  return out;
172
172
  }
173
173
 
174
- // 텍스트 파일인지 — 앞부분에 NUL 이 있으면 바이너리로 본다.
175
- export function isText(path) {
176
- try {
177
- const fd = readFileSync(path);
178
- return !fd.subarray(0, 8000).includes(0);
179
- } catch { return false; }
180
- }
181
-
182
174
  /**
183
175
  * 글 파일을 읽는다. 무엇으로 쓰여 있든 알아보고 읽는다.
184
176
  *