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
package/src/ui/md.js ADDED
@@ -0,0 +1,227 @@
1
+ // 모델 답을 사람이 읽는 모양으로 그린다.
2
+ //
3
+ // ── 왜 만드나 ───────────────────────────────────────────────────────────
4
+ //
5
+ // 여태 답을 **글자 그대로** 찍었다. 모델은 마크다운으로 말하는데 화면은 그걸
6
+ // 모르니, 실제로 이렇게 보였다.
7
+ //
8
+ // ▌ ## 고친 것
9
+ // ▌ **src/runner.js** 의 `console.log` 를 바꿨습니다.
10
+ // ▌ - 로그 형식 통일
11
+ // ▌ ```js
12
+ // ▌ log.info('x')
13
+ // ▌ ```
14
+ //
15
+ // 별표와 백틱과 우물정자가 글에 섞여 눈이 걸린다. 무엇이 제목이고 무엇이
16
+ // 코드인지 사람이 머릿속에서 다시 파싱해야 한다. 답이 길수록 심해진다.
17
+ //
18
+ // ── 왜 줄 단위인가 ─────────────────────────────────────────────────────
19
+ //
20
+ // 답은 토막토막 흘러온다. `**굵` 까지 왔을 때는 그게 굵은 글씨가 될지 알 수 없다.
21
+ // 한 번 찍은 글자는 되돌릴 수 없으니, **줄이 끝나야** 그릴 수 있다.
22
+ //
23
+ // 그런데 줄이 안 끝나면? 긴 문단 하나가 통째로 한 줄이면 몇 초 동안 화면에
24
+ // 아무것도 안 나온다 — 사람 눈에는 멈춘 것으로 보인다. 그래서 한 줄이 화면 폭을
25
+ // 넘게 길어지면 거기까지를 **날것 그대로** 흘려보낸다. 모양은 못 내지만 글은
26
+ // 계속 흐른다. 모양보다 '살아 있다' 가 먼저다.
27
+ //
28
+ // 실제로는 제목·목록·코드펜스가 다 짧은 줄이라 거의 다 그려진다. 안 그려지는
29
+ // 것은 긴 산문뿐인데, 산문은 원래 꾸밀 것이 없다.
30
+ import { c, width } from './ansi.js';
31
+
32
+ const 굵게 = (s) => c.bold(s);
33
+ const 코드 = (s) => c.hyellow(s);
34
+
35
+ // 줄 전체를 흐리게 할 때 쓴다.
36
+ //
37
+ // 그냥 c.gray() 로 감싸면 안 된다. 안쪽 꾸밈(링크·코드)이 제 색을 끝내면서
38
+ // 리셋을 남기는데, 그 리셋이 바깥 흐림까지 같이 꺼 버린다 — 한 줄이 반만
39
+ // 흐려 보인다. 그래서 안쪽이 색을 끝낼 때마다 흐림을 다시 켠다.
40
+ const 회색앞 = c.gray('·').replace(/·[\s\S]*$/, '');
41
+ const 이어흐리게 = (s) => c.gray(String(s).replace(/\x1b\[0m/g, `\x1b[0m${회색앞}`));
42
+
43
+ /**
44
+ * 한 줄 안쪽 꾸밈. `코드` → **굵게** → *기울임* → ~~취소~~ → [글](주소) 차례.
45
+ *
46
+ * 코드부터 하는 이유: 코드 안에 별표가 들어 있으면(`a ** b`) 굵게 처리가 먼저
47
+ * 먹어서 코드가 깨진다. 먼저 뽑아서 자리표시자로 빼 두고 맨 끝에 되돌린다.
48
+ */
49
+ export function 인라인(글) {
50
+ // 들어온 NUL 은 먼저 버린다. 아래에서 코드 조각을 빼 둘 때 NUL 을 자리표시자로
51
+ // 쓰는데, 답에 NUL 이 섞여 오면(도구가 읽은 바이너리를 되읊는 경우) 그것이
52
+ // 자리표시자로 읽혀 엉뚱한 글자로 바뀐다 — 모델이 한 말과 화면이 달라진다.
53
+ let s = String(글 ?? '').replace(/\x00/g, '');
54
+ const 뺀것 = [];
55
+ s = s.replace(/`([^`]+)`/g, (_, 안) => {
56
+ 뺀것.push(코드(안));
57
+ return `\x00${뺀것.length - 1}\x00`;
58
+ });
59
+
60
+ s = s.replace(/\*\*([^*]+)\*\*/g, (_, 안) => 굵게(안));
61
+ s = s.replace(/(^|[\s(])\*([^*\s][^*]*)\*(?=$|[\s.,)])/g, (_, 앞, 안) => `${앞}${c.italic(안)}`);
62
+ s = s.replace(/~~([^~]+)~~/g, (_, 안) => c.gray(안));
63
+ // 링크는 글만 보이고 주소는 흐리게. 주소를 지우면 따라갈 수가 없다.
64
+ s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, 글자, 주소) => `${c.under(글자)} ${c.gray(주소)}`);
65
+
66
+ return s.replace(/\x00(\d+)\x00/g, (_, i) => 뺀것[Number(i)]);
67
+ }
68
+
69
+ /**
70
+ * 줄 하나를 그린다.
71
+ *
72
+ * @param {string} 줄
73
+ * @param {object} 상태 {펜스: boolean} — 코드 블록 안인가
74
+ * @param {number} 폭
75
+ * @returns {{글: string, 상태: object}}
76
+ */
77
+ export function 한줄(줄, 상태 = { 펜스: false }, 폭 = 80) {
78
+ // 줄 끝의 \r 를 먼저 뗀다. 서버가 \r\n 으로 흘려보내면 여기까지 딸려 오는데,
79
+ // 아래 정규식들이 다 `(.*)$` 로 끝나고 `.` 은 \r 를 안 먹는다 — 그래서 제목도
80
+ // 목록도 하나도 안 잡히고 별표와 우물정자가 그대로 화면에 뜬다.
81
+ const s = String(줄 ?? '').replace(/\r+$/, '');
82
+
83
+ // 코드 울타리. 여는 줄에 언어가 붙어 있으면 그걸 알려 준다.
84
+ const 울타리 = s.match(/^\s*```(.*)$/);
85
+ if (울타리) {
86
+ const 안쪽 = 울타리[1].trim();
87
+ return {
88
+ 글: 상태.펜스 ? c.gray('└' + '─'.repeat(Math.max(2, Math.min(30, 폭 - 6))))
89
+ : c.gray('┌' + '─'.repeat(Math.max(2, Math.min(30, 폭 - 6))) + (안쪽 ? ` ${안쪽}` : '')),
90
+ 상태: { ...상태, 펜스: !상태.펜스 },
91
+ };
92
+ }
93
+
94
+ // 코드 안에서는 아무것도 해석하지 않는다. 별표는 별표고 우물정자는 우물정자다.
95
+ if (상태.펜스) return { 글: `${c.gray('│')} ${c.white(s)}`, 상태 };
96
+
97
+ // 가로줄
98
+ if (/^\s*([-*_])\s*\1\s*\1[\s\-*_]*$/.test(s)) {
99
+ return { 글: c.gray('─'.repeat(Math.max(4, Math.min(40, 폭 - 6)))), 상태 };
100
+ }
101
+
102
+ // 제목. 굵기와 색으로 가른다 — 우물정자를 남기면 그게 제일 먼저 눈에 띈다.
103
+ const 제목 = s.match(/^(#{1,6})\s+(.*)$/);
104
+ if (제목) {
105
+ const 깊이 = 제목[1].length;
106
+ const 글자 = 인라인(제목[2]);
107
+ if (깊이 <= 2) return { 글: `${c.hcyan('▍')} ${c.bold(글자)}`, 상태 };
108
+ return { 글: c.bold(글자), 상태 };
109
+ }
110
+
111
+ // 인용
112
+ const 인용 = s.match(/^\s*>\s?(.*)$/);
113
+ if (인용) return { 글: `${c.gray('│')} ${이어흐리게(인라인(인용[1]))}`, 상태 };
114
+
115
+ // 할 일 목록은 목록보다 먼저 본다 — `- [ ]` 가 그냥 목록으로 먹히면 안 된다.
116
+ const 할일 = s.match(/^(\s*)[-*+]\s+\[([ xX])\]\s+(.*)$/);
117
+ if (할일) {
118
+ const 표 = 할일[2] === ' ' ? c.gray('☐') : c.green('☑');
119
+ return { 글: `${할일[1]}${표} ${인라인(할일[3])}`, 상태 };
120
+ }
121
+
122
+ // 글머리표
123
+ const 목록 = s.match(/^(\s*)[-*+]\s+(.*)$/);
124
+ if (목록) return { 글: `${목록[1]}${c.hcyan('•')} ${인라인(목록[2])}`, 상태 };
125
+
126
+ // 번호 목록. 번호는 살린다 — 순서가 뜻을 갖는 자리다.
127
+ const 번호 = s.match(/^(\s*)(\d+)([.)])\s+(.*)$/);
128
+ if (번호) return { 글: `${번호[1]}${c.hcyan(번호[2] + 번호[3])} ${인라인(번호[4])}`, 상태 };
129
+
130
+ // 표는 세로줄만 흐리게 한다. 칸을 맞추려면 표 전체를 들고 있어야 하는데,
131
+ // 그러면 표가 끝날 때까지 화면에 아무것도 안 나온다.
132
+ if (/^\s*\|.*\|\s*$/.test(s)) {
133
+ // 가름줄(`|---|---|`). 그대로 두면 화면에 작대기가 그대로 뜬다.
134
+ // 바로 앞 머리줄의 칸 너비를 기억해 뒀다가 그 자리에 맞춰 긋는다 —
135
+ // 그래야 세로줄과 가로줄이 만나서 표처럼 보인다.
136
+ if (/^\s*\|[\s:|-]+\|\s*$/.test(s)) {
137
+ const 너비 = 상태.표너비 ?? s.split('|').map(() => 3);
138
+ return { 글: c.gray(너비.map((w) => '─'.repeat(w)).join('┼')), 상태 };
139
+ }
140
+ // 칸에 먼저 꾸밈을 먹이고 그 다음에 잇는다. 순서가 반대면 색 제어문자가
141
+ // 이미 낀 글에 정규식을 돌리게 되어 칸이 통째로 망가진다.
142
+ const 칸들 = s.split('|').map((칸) => 인라인(칸));
143
+ return { 글: 칸들.join(c.gray('│')), 상태: { ...상태, 표너비: 칸들.map((칸) => width(칸)) } };
144
+ }
145
+
146
+ return { 글: 인라인(s), 상태 };
147
+ }
148
+
149
+ /**
150
+ * 흘러오는 토막을 받아 그릴 수 있게 된 줄만 내놓는다.
151
+ *
152
+ * 쓰는 쪽은 이렇게 한다:
153
+ * for (const 줄 of md.넣기(토막)) 화면에 찍는다
154
+ * ... 답이 끝나면
155
+ * for (const 줄 of md.끝()) 화면에 찍는다
156
+ */
157
+ export class 마크다운 {
158
+ constructor({ 폭 = 80 } = {}) {
159
+ // 창이 좁으면 부르는 쪽이 (columns - 6) 을 넘기는데, 그게 0 이나 음수가 될 수
160
+ // 있다. 그러면 모든 줄이 '폭을 넘는다' 가 되어 **아무것도 안 그려진다.**
161
+ // 바닥을 둬서, 좁은 창에서도 짧은 줄은 제 모양으로 나가게 한다.
162
+ this.폭 = Math.max(20, Number(폭) || 0);
163
+ this.담긴것 = '';
164
+ this.상태 = { 펜스: false };
165
+ // 이 줄은 이미 날것으로 흘려보내기 시작했다 — 뒤늦게 꾸미면 앞뒤가 안 맞는다.
166
+ this.날것중 = false;
167
+ }
168
+
169
+ #그리기(줄) {
170
+ // 이 줄은 이미 날것으로 나가기 시작했다. 뒤늦게 꾸미면 앞뒤가 안 맞으므로
171
+ // 남은 만큼을 그대로 이어 붙이고 줄만 끝낸다.
172
+ if (this.날것중) { this.날것중 = false; return { 이어붙임: String(줄).replace(/\r/g, ''), 끝났나: true }; }
173
+ const r = 한줄(줄, this.상태, this.폭);
174
+ this.상태 = r.상태;
175
+ return r.글;
176
+ }
177
+
178
+ 넣기(토막) {
179
+ this.담긴것 += String(토막 ?? '');
180
+ const 나갈것 = [];
181
+
182
+ for (;;) {
183
+ const i = this.담긴것.indexOf('\n');
184
+ if (i >= 0) {
185
+ // \r\n 으로 온 줄은 여기서 \r 를 뗀다. 남겨서 내보내면 터미널에서 커서가
186
+ // 줄 맨 앞으로 돌아가 앞에 세운 세로줄 위에 덮어쓴다 — 글자가 먹힌 것처럼
187
+ // 보인다. 토막이 \r 와 \n 사이에서 끊겨 와도 여기로 모여서 걸린다.
188
+ 나갈것.push(this.#그리기(this.담긴것.slice(0, i)));
189
+ this.담긴것 = this.담긴것.slice(i + 1);
190
+ continue;
191
+ }
192
+ // 줄이 안 끝나는데 너무 길어졌다. 여기까지 날것으로 흘려보낸다 —
193
+ // 멈춘 것처럼 보이는 것보다 안 꾸며진 글이 흐르는 편이 낫다.
194
+ if (width(this.담긴것) > this.폭) {
195
+ // 날것으로 나가는 길에는 한줄() 이 안 끼므로 여기서 \r 를 턴다.
196
+ 나갈것.push({ 이어붙임: this.담긴것.replace(/\r/g, ''), 첫조각: !this.날것중, 끝났나: false });
197
+ this.담긴것 = '';
198
+ this.날것중 = true;
199
+ }
200
+ break;
201
+ }
202
+ return 나갈것;
203
+ }
204
+
205
+ /** 답이 끝났다. 남은 것을 내놓는다. */
206
+ 끝() {
207
+ // 담긴 것은 없는데 날것으로 나가던 중이면 줄만 끝내 준다.
208
+ if (!this.담긴것) {
209
+ const 닫을것 = this.날것중 ? [{ 이어붙임: '', 끝났나: true }] : [];
210
+ this.날것중 = false;
211
+ return 닫을것;
212
+ }
213
+ const 남은것 = this.#그리기(this.담긴것);
214
+ this.담긴것 = '';
215
+ this.날것중 = false;
216
+ return [남은것];
217
+ }
218
+ }
219
+
220
+ /** 통째로 있는 글을 한 번에 그린다 (스트리밍이 아닌 자리). */
221
+ export function 그리기(글, 폭 = 80) {
222
+ const md = new 마크다운({ 폭 });
223
+ const 줄들 = [];
224
+ for (const x of md.넣기(String(글 ?? ''))) 줄들.push(typeof x === 'string' ? x : x.이어붙임);
225
+ for (const x of md.끝()) 줄들.push(typeof x === 'string' ? x : x.이어붙임);
226
+ return 줄들;
227
+ }
@@ -0,0 +1,101 @@
1
+ /*
2
+ * 끝났을 때 알리기 — 종소리와 창 제목.
3
+ *
4
+ * 로컬 모델은 느리다. 7B 를 CPU 로 돌리면 한 턴에 2~3분이 예사고, 그동안
5
+ * 사람은 다른 창으로 간다. 그러다 돌아와 보면 이미 5분 전에 끝나 있거나,
6
+ * 더 나쁘게는 "실행할까요?" 에서 3분째 멈춰 서 있다 — 물어본 줄을 몰라서.
7
+ *
8
+ * 클라우드 에이전트는 몇 초 만에 끝나니 알림이 없어도 됐지만, 여기서는
9
+ * 그것이 곧 기다린 시간이 된다. 그래서 딱 두 가지만 한다.
10
+ *
11
+ * 종소리 다 됐을 때, 그리고 **물어볼 때**. 물어볼 때가 더 중요하다 —
12
+ * 끝난 것은 늦게 알아도 되지만, 막혀 있는 것은 그만큼 손해다.
13
+ * 창 제목 탭 이름만 봐도 도는 중인지 끝났는지 알게 한다.
14
+ *
15
+ * ── 어디로 내보내나 ─────────────────────────────────────────────────────
16
+ *
17
+ * stdout 이 터미널이면 stdout, 아니면 stderr. 둘 다 아니면 아무것도 안 한다.
18
+ *
19
+ * 이 순서가 중요하다. ACP(에디터 안에서 쓰기)와 `deel run` 은 stdout 이
20
+ * 파이프다. 거기에 \x07 이나 \x1b]2; 를 흘리면 JSON-RPC 한 줄이 깨져서
21
+ * 에디터가 세션을 통째로 끊는다. 소리 한 번 내려다 연결을 끊는 셈이다.
22
+ * isTTY 를 보는 이유가 이것이고, 여기서만은 '아마 괜찮겠지' 로 안 넘어간다.
23
+ */
24
+
25
+ /** 끄는 스위치. 켜 두면 소리도 제목도 없다. 조용한 사무실용. */
26
+ export const 끔 = () => !!(process.env.DEEL_NO_BELL || process.env.NO_BELL);
27
+
28
+ /**
29
+ * 몇 초 넘게 걸린 턴만 알린다.
30
+ *
31
+ * "안녕" 에 1초 만에 답할 때마다 딩 소리가 나면, 사람은 이틀 만에 알림을 끈다.
32
+ * 그러면 정작 3분짜리 턴도 못 듣는다. 알림은 아껴 써야 알림이다.
33
+ */
34
+ export const 알릴만한초 = 12;
35
+
36
+ function 내보낼곳() {
37
+ if (끔()) return null;
38
+ if (process.stdout?.isTTY) return process.stdout;
39
+ if (process.stderr?.isTTY) return process.stderr;
40
+ return null;
41
+ }
42
+
43
+ function 쓰기(글) {
44
+ const 곳 = 내보낼곳();
45
+ if (!곳) return false;
46
+ try { 곳.write(글); return true; } catch { return false; }
47
+ }
48
+
49
+ /** 종을 한 번 울린다. @returns {boolean} 실제로 울렸나 */
50
+ export function 종() {
51
+ return 쓰기('\x07');
52
+ }
53
+
54
+ /**
55
+ * 창 제목을 바꾼다.
56
+ *
57
+ * OSC 2 (제목만) 와 OSC 1 (아이콘 이름) 을 같이 보낸다. 터미널마다 보는
58
+ * 것이 달라서 — 윈도우 터미널·iTerm 은 2를, 어떤 tmux 설정은 1을 본다.
59
+ * 끝맺음은 BEL(\x07) 대신 ST(\x1b\\) 를 쓴다. BEL 로 끝내면 제목을 바꿀
60
+ * 때마다 종이 같이 울려서, 위에서 아껴 쓰기로 한 소리가 매초 난다.
61
+ */
62
+ export function 창제목(글) {
63
+ const s = String(글 ?? '').replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 120);
64
+ return 쓰기(`\x1b]2;${s}\x1b\\\x1b]1;${s}\x1b\\`);
65
+ }
66
+
67
+ /** 켤 때 있던 제목으로 돌려놓는다. 남의 터미널을 우리 이름으로 두고 나가지 않는다. */
68
+ export function 제목되돌리기() {
69
+ return 쓰기('\x1b]2;\x1b\\\x1b]1;\x1b\\');
70
+ }
71
+
72
+ /**
73
+ * 이 턴을 알릴 것인가.
74
+ *
75
+ * @param {object} o
76
+ * @param {number} o.걸린밀리초
77
+ * @param {boolean} [o.물어봄] 사람 답을 기다리는 자리인가. 그러면 시간과 무관하게 알린다.
78
+ * @param {boolean} [o.켬] 설정에서 켰나 (기본 켬)
79
+ * @param {number} [o.문턱초]
80
+ */
81
+ export function 알릴까({ 걸린밀리초 = 0, 물어봄 = false, 켬 = true, 문턱초 = 알릴만한초 } = {}) {
82
+ if (!켬 || 끔()) return false;
83
+ if (물어봄) return true;
84
+ return 걸린밀리초 >= 문턱초 * 1000;
85
+ }
86
+
87
+ /**
88
+ * 상태에 맞는 창 제목 글을 짓는다.
89
+ *
90
+ * 폴더 이름을 앞에 붙인다. 창을 여럿 띄워 놓고 쓰는 사람이 많은데, 전부
91
+ * "deel" 이면 어느 탭이 끝난 건지 알 수가 없다. 탭 이름은 짧게 잘리니
92
+ * 구분되는 것(폴더)을 앞에, 공통인 것(deel)을 뒤에 둔다.
93
+ */
94
+ export function 제목글(갈래, { 폴더 = '', 초 = 0 } = {}) {
95
+ const 이름 = 폴더 ? `${폴더} · ` : '';
96
+ if (갈래 === '도는중') return `${이름}deel ${초 >= 60 ? `${Math.floor(초 / 60)}분` : `${Math.max(0, Math.round(초))}초`}`;
97
+ if (갈래 === '물어봄') return `${이름}deel — 물어볼 것이 있습니다`;
98
+ if (갈래 === '끝남') return `${이름}deel — 다 됐습니다`;
99
+ if (갈래 === '탈남') return `${이름}deel — 멈췄습니다`;
100
+ return `${이름}deel`;
101
+ }
package/src/ui/prompt.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // 입력 받기 — 한 줄, 비밀번호(가림), 목록 선택, 예/아니오.
2
- import { c, say, mark, cursor } from './ansi.js';
2
+ import { c, say } from './ansi.js';
3
3
 
4
4
  function raw() {
5
5
  return process.stdin.isTTY ? process.stdin.setRawMode.bind(process.stdin) : null;
package/src/ui/screen.js CHANGED
@@ -30,7 +30,7 @@
30
30
  * 대화는 그냥 흘려보내기로 했다. 그러면 stdout 에 쓰는 모든 코드가 손 안 대고
31
31
  * 그대로 맞다. 스크롤·복사·`| tee` 도 그대로다.
32
32
  */
33
- import { c, say, cursor, box, cols } from './ansi.js';
33
+ import { c, say, cursor, box } from './ansi.js';
34
34
  import { statusLine, contextWarning } from './status.js';
35
35
  import { spin } from './spinner.js';
36
36